@serve.zone/appstore 32.27.0 → 33.1.0
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/.smartconfig.json +4 -1
- package/changelog.md +70 -0
- package/dist_catalog/catalog.digest +1 -0
- package/dist_catalog/catalog.envelope.json +7617 -0
- package/{dist_ts_client/data.generated.js → dist_catalog/catalog.json} +6912 -6908
- package/dist_ts_client/catalog.d.ts +16 -0
- package/dist_ts_client/catalog.js +12 -0
- package/dist_ts_client/index.d.ts +3 -1
- package/dist_ts_client/index.js +4 -2
- package/dist_ts_client/loader.d.ts +69 -0
- package/dist_ts_client/loader.js +361 -0
- package/dist_ts_client/plugins.d.ts +5 -0
- package/dist_ts_client/plugins.js +8 -0
- package/dist_ts_client/projection.d.ts +59 -0
- package/dist_ts_client/projection.js +83 -0
- package/dist_ts_client/storage.d.ts +15 -9
- package/dist_ts_client/storage.js +81 -657
- package/dist_ts_client/types.d.ts +86 -41
- package/dist_ts_client/types.js +1 -1
- package/dist_ts_testing/fixtures.d.ts +83 -0
- package/dist_ts_testing/fixtures.js +146 -0
- package/dist_ts_testing/index.d.ts +1 -0
- package/dist_ts_testing/index.js +2 -0
- package/dist_ts_testing/plugins.d.ts +7 -0
- package/dist_ts_testing/plugins.js +11 -0
- package/package.json +16 -11
- package/readme.md +162 -93
- package/ts_client/catalog.ts +26 -0
- package/ts_client/index.ts +3 -1
- package/ts_client/loader.ts +414 -0
- package/ts_client/plugins.ts +10 -0
- package/ts_client/projection.ts +148 -0
- package/ts_client/readme.md +17 -27
- package/ts_client/storage.ts +93 -882
- package/ts_client/types.ts +102 -42
- package/ts_testing/fixtures.ts +230 -0
- package/ts_testing/index.ts +1 -0
- package/ts_testing/plugins.ts +15 -0
- package/ts_testing/tspublish.json +3 -0
- package/apps/adminer/app.json +0 -10
- package/apps/adminer/versions/1.0.0/config.json +0 -6
- package/apps/cloudly/app.json +0 -14
- package/apps/cloudly/versions/1.0.0/config.json +0 -120
- package/apps/cloudly/versions/1.1.0/config.json +0 -120
- package/apps/cloudly/versions/1.1.1/config.json +0 -120
- package/apps/cloudly/versions/1.1.10/config.json +0 -173
- package/apps/cloudly/versions/1.1.2/config.json +0 -130
- package/apps/cloudly/versions/1.1.3/config.json +0 -130
- package/apps/cloudly/versions/1.1.4/config.json +0 -130
- package/apps/cloudly/versions/1.1.5/config.json +0 -149
- package/apps/cloudly/versions/1.1.6/config.json +0 -149
- package/apps/cloudly/versions/1.1.7/config.json +0 -149
- package/apps/cloudly/versions/1.1.8/config.json +0 -173
- package/apps/cloudly/versions/1.1.9/config.json +0 -173
- package/apps/cloudly/versions/1.10.0/config.json +0 -173
- package/apps/cloudly/versions/1.11.0/config.json +0 -173
- package/apps/cloudly/versions/1.12.0/config.json +0 -173
- package/apps/cloudly/versions/1.13.0/config.json +0 -173
- package/apps/cloudly/versions/1.14.0/config.json +0 -173
- package/apps/cloudly/versions/1.15.0/config.json +0 -173
- package/apps/cloudly/versions/1.16.0/config.json +0 -173
- package/apps/cloudly/versions/1.17.0/config.json +0 -173
- package/apps/cloudly/versions/1.18.0/config.json +0 -173
- package/apps/cloudly/versions/1.19.0/config.json +0 -173
- package/apps/cloudly/versions/1.2.0/config.json +0 -173
- package/apps/cloudly/versions/1.2.1/config.json +0 -173
- package/apps/cloudly/versions/1.20.0/config.json +0 -173
- package/apps/cloudly/versions/1.21.0/config.json +0 -173
- package/apps/cloudly/versions/1.22.0/config.json +0 -173
- package/apps/cloudly/versions/1.23.0/config.json +0 -173
- package/apps/cloudly/versions/1.24.0/config.json +0 -173
- package/apps/cloudly/versions/1.25.0/config.json +0 -173
- package/apps/cloudly/versions/1.26.0/config.json +0 -173
- package/apps/cloudly/versions/1.27.0/config.json +0 -173
- package/apps/cloudly/versions/1.3.0/config.json +0 -173
- package/apps/cloudly/versions/1.4.0/config.json +0 -173
- package/apps/cloudly/versions/1.5.0/config.json +0 -173
- package/apps/cloudly/versions/1.6.0/config.json +0 -173
- package/apps/cloudly/versions/1.7.0/config.json +0 -173
- package/apps/cloudly/versions/1.8.0/config.json +0 -173
- package/apps/cloudly/versions/1.9.0/config.json +0 -173
- package/apps/ghost/app.json +0 -10
- package/apps/ghost/versions/1.0.0/config.json +0 -18
- package/apps/gitea/app.json +0 -10
- package/apps/gitea/versions/1.0.0/config.json +0 -16
- package/apps/gitops/app.json +0 -14
- package/apps/gitops/versions/1.0.0/config.json +0 -28
- package/apps/grafana/app.json +0 -10
- package/apps/grafana/versions/1.0.0/config.json +0 -9
- package/apps/mariadb/app.json +0 -10
- package/apps/mariadb/versions/1.0.0/config.json +0 -9
- package/apps/mattermost/app.json +0 -10
- package/apps/mattermost/versions/1.0.0/config.json +0 -6
- package/apps/n8n/app.json +0 -10
- package/apps/n8n/versions/1.0.0/config.json +0 -6
- package/apps/nextcloud/app.json +0 -10
- package/apps/nextcloud/versions/1.0.0/config.json +0 -16
- package/apps/nginx/app.json +0 -10
- package/apps/nginx/versions/1.0.0/config.json +0 -6
- package/apps/openbao/app.json +0 -16
- package/apps/openbao/versions/1.0.0/config.json +0 -52
- package/apps/plausible/app.json +0 -10
- package/apps/plausible/versions/1.0.0/config.json +0 -9
- package/apps/portainer/app.json +0 -10
- package/apps/portainer/versions/1.0.0/config.json +0 -6
- package/apps/postgres/app.json +0 -10
- package/apps/postgres/versions/1.0.0/config.json +0 -11
- package/apps/rustdesk-server/app.json +0 -14
- package/apps/rustdesk-server/versions/1.0.0/config.json +0 -19
- package/apps/siprouter/app.json +0 -14
- package/apps/siprouter/versions/1.0.0/config.json +0 -84
- package/apps/uptime-kuma/app.json +0 -10
- package/apps/uptime-kuma/versions/1.0.0/config.json +0 -6
- package/apps/valkey/app.json +0 -10
- package/apps/valkey/versions/1.0.0/config.json +0 -6
- package/apps/vaultwarden/app.json +0 -10
- package/apps/vaultwarden/versions/1.0.0/config.json +0 -6
- package/apps/wordpress/app.json +0 -10
- package/apps/wordpress/versions/1.0.0/config.json +0 -15
- package/appstore.json +0 -194
- package/dist_ts_client/classes.appstoreresolver.d.ts +0 -59
- package/dist_ts_client/classes.appstoreresolver.js +0 -736
- package/dist_ts_client/data.generated.d.ts +0 -2
- package/ts_client/classes.appstoreresolver.ts +0 -896
- package/ts_client/data.generated.ts +0 -7606
package/ts_client/types.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import type * as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export type TAppStoreIndex = interfaces.appstore.IAppStoreIndex;
|
|
6
|
-
export type TAppStoreApp = interfaces.appstore.IAppStoreApp;
|
|
7
|
-
export type TAppStoreAppMeta = interfaces.appstore.IAppStoreAppMeta;
|
|
8
|
-
export type TAppStoreVersionConfig = interfaces.appstore.IAppStoreVersionConfig;
|
|
9
|
-
export type TAppStoreDockerImageSource = interfaces.appstore.IAppStoreDockerImageSource;
|
|
10
|
-
export type TAppStoreRepoManifestSource = interfaces.appstore.IAppStoreRepoManifestSource;
|
|
11
|
-
export type TAppStoreResolvedSource = interfaces.appstore.IAppStoreResolvedSource;
|
|
12
|
-
export type TServezoneAppStoreManifest = interfaces.appstore.IServezoneAppStoreManifest;
|
|
13
|
-
export type TAppStoreStorageRequest = interfaces.appstore.TAppStoreStorageRequest;
|
|
14
|
-
export type TAppStoreStorageClass = interfaces.appstore.IAppStoreStorageClass;
|
|
15
|
-
export type TAppStoreStorageFeatureId = interfaces.appstore.TAppStoreStorageFeatureId;
|
|
1
|
+
import type * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------------------------
|
|
4
|
+
// Storage handoff
|
|
16
5
|
|
|
17
6
|
export type TAppStoreStorageRequestOrigin =
|
|
18
7
|
| 'explicit'
|
|
@@ -32,11 +21,12 @@ export interface IAppStoreStorageLegacyCompatibility {
|
|
|
32
21
|
};
|
|
33
22
|
}
|
|
34
23
|
|
|
24
|
+
/** The canonical storage handoff a fulfillment adapter receives for one version config. */
|
|
35
25
|
export interface INormalizedAppStoreStorageConfig {
|
|
36
|
-
storageClasses: Record<string,
|
|
37
|
-
storageRequests: TAppStoreStorageRequest[];
|
|
26
|
+
storageClasses: Record<string, plugins.servezoneInterfaces.appstore.IAppStoreStorageClass>;
|
|
27
|
+
storageRequests: plugins.servezoneInterfaces.appstore.TAppStoreStorageRequest[];
|
|
38
28
|
requestOrigins: Record<string, TAppStoreStorageRequestOrigin>;
|
|
39
|
-
requiredFeatureIds: TAppStoreStorageFeatureId[];
|
|
29
|
+
requiredFeatureIds: plugins.servezoneInterfaces.appstore.TAppStoreStorageFeatureId[];
|
|
40
30
|
legacyCompatibility?: IAppStoreStorageLegacyCompatibility;
|
|
41
31
|
}
|
|
42
32
|
|
|
@@ -48,37 +38,107 @@ export interface IAppStoreStorageNormalizationOptions {
|
|
|
48
38
|
supportedFeatures: Iterable<string>;
|
|
49
39
|
}
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
// ---------------------------------------------------------------------------------------------
|
|
42
|
+
// Catalog loading
|
|
43
|
+
|
|
44
|
+
export interface IAppStoreCatalogLoaderOptions {
|
|
45
|
+
/** The catalog signing keys this consumer trusts, shipped with its release, never fetched. */
|
|
46
|
+
trustedKeys: readonly plugins.servezoneInterfaces.appstore.IAppStoreCatalogTrustedKey[];
|
|
47
|
+
/** The feature ids this consumer fulfils (`requiresFeatures`), e.g. `appStoreStorageFeatureIds` values. */
|
|
48
|
+
supportedFeatureIds: Iterable<string>;
|
|
49
|
+
/**
|
|
50
|
+
* npm registries asked for the catalog package, in order, as
|
|
51
|
+
* `appstore.validateAppStoreCatalogRegistries` (`@serve.zone/interfaces`) admits them. Absent or
|
|
52
|
+
* empty means `appstore.appStoreCatalogDefaultRegistries` (npmjs).
|
|
53
|
+
*/
|
|
54
|
+
registries?: readonly string[];
|
|
55
|
+
/** Defaults to the global `fetch`. */
|
|
56
|
+
fetch?: typeof fetch;
|
|
57
|
+
/** Defaults to the system clock. */
|
|
57
58
|
now?: () => Date;
|
|
59
|
+
/** Bound of each registry request, response body included. Defaults to 30 000 ms. */
|
|
60
|
+
requestTimeoutMs?: number;
|
|
61
|
+
/** Largest package tarball the loader downloads. Defaults to 32 MiB. */
|
|
62
|
+
maximumTarballBytes?: number;
|
|
63
|
+
/** Largest size the loader decompresses a package tarball to. Defaults to 256 MiB. */
|
|
64
|
+
maximumUnpackedBytes?: number;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
/** Options of one load. */
|
|
68
|
+
export interface IAppStoreCatalogLoadOptions {
|
|
69
|
+
/**
|
|
70
|
+
* Aborting it (the consumer stopping) ends the load at once, in-flight requests included, and
|
|
71
|
+
* rejects it with the signal's reason.
|
|
72
|
+
*/
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What a consumer stored with its last-known-good envelope text, to restore it at start. */
|
|
77
|
+
export interface IAppStoreCatalogStoredRecord {
|
|
78
|
+
origin: plugins.servezoneInterfaces.appstore.TAppStoreCatalogOrigin;
|
|
79
|
+
/** When the consumer first activated it. */
|
|
80
|
+
loadedAt: string;
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
/** What one registry answered during a load. */
|
|
84
|
+
export interface IAppStoreCatalogRegistryReport {
|
|
85
|
+
registry: string;
|
|
86
|
+
outcome: plugins.servezoneInterfaces.appstore.TAppStoreCatalogAttemptOutcome;
|
|
87
|
+
/** The package version the registry's `latest` dist-tag named, once read. */
|
|
88
|
+
packageVersion?: string;
|
|
89
|
+
/** Why a `refused` or `failed` answer ended. */
|
|
90
|
+
message?: string;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
configsByVersion: Map<string, TAppStoreVersionConfig>;
|
|
93
|
+
interface IAppStoreCatalogJudgementBase {
|
|
94
|
+
/** The attempt as a consumer records it in its catalog status (`lastAttempt`). */
|
|
95
|
+
attempt: plugins.servezoneInterfaces.appstore.IAppStoreCatalogAttempt;
|
|
77
96
|
}
|
|
78
97
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
/** A verified catalog that succeeds the active one; the consumer stores it as last-known-good. */
|
|
99
|
+
export interface IAppStoreCatalogActivation extends IAppStoreCatalogJudgementBase {
|
|
100
|
+
outcome: 'activated';
|
|
101
|
+
envelope: plugins.servezoneInterfaces.appstore.IAppStoreCatalogEnvelope;
|
|
102
|
+
/** The status `active` member for the activated catalog. */
|
|
103
|
+
active: plugins.servezoneInterfaces.appstore.IAppStoreCatalogActiveState;
|
|
84
104
|
}
|
|
105
|
+
|
|
106
|
+
/** The candidate is the active catalog. */
|
|
107
|
+
export interface IAppStoreCatalogUnchanged extends IAppStoreCatalogJudgementBase {
|
|
108
|
+
outcome: 'unchanged';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The candidate was read and refused; the active catalog stays in place. */
|
|
112
|
+
export interface IAppStoreCatalogRefused extends IAppStoreCatalogJudgementBase {
|
|
113
|
+
outcome: 'refused';
|
|
114
|
+
/** The status `refused` member. */
|
|
115
|
+
refused: plugins.servezoneInterfaces.appstore.IAppStoreCatalogRefusalRecord;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** No candidate could be read: transport, integrity or packaging failed. */
|
|
119
|
+
export interface IAppStoreCatalogFailure extends IAppStoreCatalogJudgementBase {
|
|
120
|
+
outcome: 'failed';
|
|
121
|
+
message: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The judgement of one candidate envelope against the active catalog. */
|
|
125
|
+
export type TAppStoreCatalogJudgement =
|
|
126
|
+
| IAppStoreCatalogActivation
|
|
127
|
+
| IAppStoreCatalogUnchanged
|
|
128
|
+
| IAppStoreCatalogRefused;
|
|
129
|
+
|
|
130
|
+
/** The result of one registry load: the judgement of the best answer and every registry's answer. */
|
|
131
|
+
export type TAppStoreCatalogLoadResult = (TAppStoreCatalogJudgement | IAppStoreCatalogFailure) & {
|
|
132
|
+
registries: IAppStoreCatalogRegistryReport[];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** A stored catalog that verifies today: the consumer activates it at start. */
|
|
136
|
+
export interface IAppStoreCatalogRestored {
|
|
137
|
+
outcome: 'restored';
|
|
138
|
+
envelope: plugins.servezoneInterfaces.appstore.IAppStoreCatalogEnvelope;
|
|
139
|
+
/** The status `active` member: `origin` and `loadedAt` as stored, `keyId` as verified now. */
|
|
140
|
+
active: plugins.servezoneInterfaces.appstore.IAppStoreCatalogActiveState;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The result of restoring a stored catalog: restored, or refused and left inactive. */
|
|
144
|
+
export type TAppStoreCatalogRestoreResult = IAppStoreCatalogRestored | IAppStoreCatalogRefused;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test fixtures for App Store catalog consumers (`@serve.zone/appstore/testing`), never part of the
|
|
3
|
+
* runtime entry: a throwaway signing key, a catalog, its signed envelope, the npm package tarball
|
|
4
|
+
* carrying it, a registry stub and a `fetch` serving it. A consumer's tests drive
|
|
5
|
+
* `AppStoreCatalogLoader` against these exactly as against a registry.
|
|
6
|
+
*/
|
|
7
|
+
import * as plugins from './plugins.js';
|
|
8
|
+
import { appStoreCatalogEnvelopePackagePath, appStoreCatalogPackageName } from '../ts_client/catalog.js';
|
|
9
|
+
|
|
10
|
+
type TCatalog = plugins.servezoneInterfaces.appstore.IAppStoreCatalog;
|
|
11
|
+
type TEnvelope = plugins.servezoneInterfaces.appstore.IAppStoreCatalogEnvelope;
|
|
12
|
+
type TVersionConfig = plugins.servezoneInterfaces.appstore.TAppStoreCatalogVersionConfig;
|
|
13
|
+
type TSigningKey = plugins.servezoneInterfacesRuntime.IAppStoreCatalogSigningKey;
|
|
14
|
+
type TTrustedKey = plugins.servezoneInterfaces.appstore.IAppStoreCatalogTrustedKey;
|
|
15
|
+
|
|
16
|
+
/** A throwaway catalog signing key and the trusted-key record a consumer under test is given. */
|
|
17
|
+
export interface IAppStoreTestSigningKey {
|
|
18
|
+
signingKey: TSigningKey;
|
|
19
|
+
trustedKey: TTrustedKey;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IAppStoreTestVersion {
|
|
23
|
+
version: string;
|
|
24
|
+
config: TVersionConfig;
|
|
25
|
+
withdrawn?: plugins.servezoneInterfaces.appstore.IAppStoreCatalogWithdrawal;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IAppStoreTestApp {
|
|
29
|
+
info: plugins.servezoneInterfaces.appstore.IServezoneAppStoreAppInfo;
|
|
30
|
+
/** Defaults to the highest version that is not withdrawn (the highest one when all are). */
|
|
31
|
+
latestVersion?: string;
|
|
32
|
+
upgradeStrategy?: plugins.servezoneInterfaces.appstore.TAppStoreCatalogUpgradeStrategy;
|
|
33
|
+
versions: IAppStoreTestVersion[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IAppStoreTestCatalogOptions {
|
|
37
|
+
revision: number;
|
|
38
|
+
/** Defaults to `2026-10-08T12:00:00.000Z`. */
|
|
39
|
+
publishedAt?: string;
|
|
40
|
+
/** Defaults to 40 times `c`. */
|
|
41
|
+
sourceRevision?: string;
|
|
42
|
+
apps: IAppStoreTestApp[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One registry origin's answers: the `latest` version manifest and the tarball it names. */
|
|
46
|
+
export interface IAppStoreTestRegistryStub {
|
|
47
|
+
/** The `latest` version manifest body; `null` answers 404. */
|
|
48
|
+
manifest: unknown;
|
|
49
|
+
tarball?: Uint8Array<ArrayBuffer>;
|
|
50
|
+
/** Never answers until the request is aborted. */
|
|
51
|
+
hang?: boolean;
|
|
52
|
+
/** Answers every request with this HTTP status. */
|
|
53
|
+
status?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const tarballPath = '/tarball.tgz';
|
|
57
|
+
|
|
58
|
+
/** A fresh throwaway key; nothing about it is ever stored. */
|
|
59
|
+
export const createAppStoreTestSigningKey = async (): Promise<IAppStoreTestSigningKey> => {
|
|
60
|
+
const signingKey = await plugins.servezoneInterfacesRuntime.createAppStoreCatalogSigningKey();
|
|
61
|
+
return { signingKey, trustedKey: plugins.servezoneInterfacesRuntime.getAppStoreCatalogTrustedKey(signingKey) };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** A version config the contract accepts: a digest-pinned `example/<name>` image and one web port. */
|
|
65
|
+
export const createAppStoreTestVersionConfig = (
|
|
66
|
+
nameArg: string,
|
|
67
|
+
imageDigitArg: string,
|
|
68
|
+
overridesArg: Partial<TVersionConfig> = {},
|
|
69
|
+
): TVersionConfig => ({
|
|
70
|
+
image: `example/${nameArg}@sha256:${imageDigitArg.repeat(64)}`,
|
|
71
|
+
targetPorts: [{ name: 'web', port: 8080, protocol: 'http', default: true }],
|
|
72
|
+
...overridesArg,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const compareVersions = (leftArg: string, rightArg: string): number => {
|
|
76
|
+
const { data } = plugins.servezoneInterfaces;
|
|
77
|
+
const left = data.parseReleaseVersion(leftArg);
|
|
78
|
+
const right = data.parseReleaseVersion(rightArg);
|
|
79
|
+
if (left === null || right === null) throw new Error(`test catalog versions must be MAJOR.MINOR.PATCH: ${leftArg}, ${rightArg}`);
|
|
80
|
+
return data.compareReleaseVersions(left, right);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const defaultLatestVersion = (appArg: IAppStoreTestApp): string => {
|
|
84
|
+
const available = appArg.versions.filter((versionArg) => versionArg.withdrawn === undefined);
|
|
85
|
+
const candidates = (available.length > 0 ? available : appArg.versions).map((versionArg) => versionArg.version);
|
|
86
|
+
const [latest] = [...candidates].sort((leftArg, rightArg) => compareVersions(rightArg, leftArg));
|
|
87
|
+
if (latest === undefined) throw new Error(`test catalog app '${appArg.info.id}' lists no version`);
|
|
88
|
+
return latest;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Every feature id the catalog's version configs name. */
|
|
92
|
+
const usedFeatureIds = (catalogArg: TCatalog): string[] =>
|
|
93
|
+
[...new Set(catalogArg.apps.flatMap((appArg) =>
|
|
94
|
+
appArg.versions.flatMap((versionArg) => versionArg.config.requiresFeatures ?? [])))].sort();
|
|
95
|
+
|
|
96
|
+
/** A catalog the contract accepts, apps in id order, each version with its config digest; throws otherwise. */
|
|
97
|
+
export const createAppStoreTestCatalog = async (optionsArg: IAppStoreTestCatalogOptions): Promise<TCatalog> => {
|
|
98
|
+
const { appstore } = plugins.servezoneInterfaces;
|
|
99
|
+
const apps: TCatalog['apps'] = [];
|
|
100
|
+
const sorted = [...optionsArg.apps].sort((leftArg, rightArg) =>
|
|
101
|
+
leftArg.info.id < rightArg.info.id ? -1 : leftArg.info.id > rightArg.info.id ? 1 : 0);
|
|
102
|
+
for (const app of sorted) {
|
|
103
|
+
const versions: TCatalog['apps'][number]['versions'] = [];
|
|
104
|
+
for (const version of app.versions) {
|
|
105
|
+
const config = structuredClone(version.config);
|
|
106
|
+
versions.push({
|
|
107
|
+
version: version.version,
|
|
108
|
+
config,
|
|
109
|
+
configDigest: await appstore.computeAppStoreCatalogConfigDigest(config),
|
|
110
|
+
...(version.withdrawn === undefined ? {} : { withdrawn: { ...version.withdrawn } }),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
apps.push({
|
|
114
|
+
info: structuredClone(app.info),
|
|
115
|
+
latestVersion: app.latestVersion ?? defaultLatestVersion(app),
|
|
116
|
+
...(app.upgradeStrategy === undefined ? {} : { upgradeStrategy: app.upgradeStrategy }),
|
|
117
|
+
versions,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const contract = appstore.appStoreCatalogContract;
|
|
121
|
+
const catalog: TCatalog = {
|
|
122
|
+
contract: { id: contract.id, major: contract.major, minor: contract.minor },
|
|
123
|
+
revision: optionsArg.revision,
|
|
124
|
+
publishedAt: optionsArg.publishedAt ?? '2026-10-08T12:00:00.000Z',
|
|
125
|
+
sourceRevision: optionsArg.sourceRevision ?? 'c'.repeat(40),
|
|
126
|
+
apps,
|
|
127
|
+
};
|
|
128
|
+
const errors = appstore.validateAppStoreCatalog(catalog, { supportedFeatureIds: usedFeatureIds(catalog) });
|
|
129
|
+
if (errors.length > 0) throw new Error(`test catalog refused by the contract: ${errors.join('; ')}`);
|
|
130
|
+
return catalog;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** The signed envelope of a test catalog, as the publisher would ship it. */
|
|
134
|
+
export const signAppStoreTestCatalog = async (catalogArg: TCatalog, keyArg: IAppStoreTestSigningKey): Promise<TEnvelope> =>
|
|
135
|
+
await plugins.servezoneInterfacesRuntime.signAppStoreCatalog(catalogArg, [keyArg.signingKey], {
|
|
136
|
+
supportedFeatureIds: usedFeatureIds(catalogArg),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const gzip = async (bytesArg: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>> =>
|
|
140
|
+
new Uint8Array(await new Response(new Blob([bytesArg]).stream().pipeThrough(new CompressionStream('gzip'))).arrayBuffer());
|
|
141
|
+
|
|
142
|
+
export interface IAppStoreTestTarballOptions {
|
|
143
|
+
/** Defaults to `33.0.0`. */
|
|
144
|
+
packageVersion?: string;
|
|
145
|
+
/** Bytes of a filler `dist_catalog/catalog.json` entry ahead of the envelope; defaults to 0. */
|
|
146
|
+
fillerBytes?: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The gzip-compressed npm package tarball (`package/…`) of `@serve.zone/appstore`, carrying
|
|
151
|
+
* `envelopeTextArg` at the envelope's package path, or no envelope for `null`.
|
|
152
|
+
*/
|
|
153
|
+
export const createAppStoreTestPackageTarball = async (
|
|
154
|
+
envelopeTextArg: string | null,
|
|
155
|
+
optionsArg: IAppStoreTestTarballOptions = {},
|
|
156
|
+
): Promise<Uint8Array<ArrayBuffer>> => {
|
|
157
|
+
const encoder = new TextEncoder();
|
|
158
|
+
const packageJson = encoder.encode(JSON.stringify({
|
|
159
|
+
name: appStoreCatalogPackageName,
|
|
160
|
+
version: optionsArg.packageVersion ?? '33.0.0',
|
|
161
|
+
}));
|
|
162
|
+
const filler = new Uint8Array(optionsArg.fillerBytes ?? 0).fill(0x20);
|
|
163
|
+
const entries = [
|
|
164
|
+
{ header: { name: 'package/package.json', size: packageJson.byteLength }, body: packageJson },
|
|
165
|
+
{ header: { name: 'package/dist_catalog/catalog.json', size: filler.byteLength }, body: filler },
|
|
166
|
+
];
|
|
167
|
+
if (envelopeTextArg !== null) {
|
|
168
|
+
const envelope = encoder.encode(envelopeTextArg);
|
|
169
|
+
entries.push({
|
|
170
|
+
header: { name: `package/${appStoreCatalogEnvelopePackagePath}`, size: envelope.byteLength },
|
|
171
|
+
body: envelope,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return await gzip(await plugins.modernTar.packTar(entries));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** The npm `dist.integrity` of a tarball. */
|
|
178
|
+
export const computeAppStoreTestIntegrity = (bytesArg: Uint8Array): string =>
|
|
179
|
+
`sha512-${plugins.crypto.createHash('sha512').update(bytesArg).digest('base64')}`;
|
|
180
|
+
|
|
181
|
+
export interface IAppStoreTestRegistryOptions {
|
|
182
|
+
/** Defaults to the tarball's true integrity. */
|
|
183
|
+
integrity?: string;
|
|
184
|
+
/** Defaults to `33.0.0`. */
|
|
185
|
+
packageVersion?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A registry stub at `originArg` whose `latest` names `tarballArg`. */
|
|
189
|
+
export const createAppStoreTestRegistry = (
|
|
190
|
+
originArg: string,
|
|
191
|
+
tarballArg: Uint8Array<ArrayBuffer>,
|
|
192
|
+
optionsArg: IAppStoreTestRegistryOptions = {},
|
|
193
|
+
): IAppStoreTestRegistryStub => ({
|
|
194
|
+
manifest: {
|
|
195
|
+
name: appStoreCatalogPackageName,
|
|
196
|
+
version: optionsArg.packageVersion ?? '33.0.0',
|
|
197
|
+
dist: { tarball: `${originArg}${tarballPath}`, integrity: optionsArg.integrity ?? computeAppStoreTestIntegrity(tarballArg) },
|
|
198
|
+
},
|
|
199
|
+
tarball: tarballArg,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A `fetch` serving one stub per registry origin: the `latest` manifest of
|
|
204
|
+
* `@serve.zone/appstore` and its tarball. An origin without a stub fails like an unreachable host;
|
|
205
|
+
* a hanging stub rejects once the request's signal aborts.
|
|
206
|
+
*/
|
|
207
|
+
export const createAppStoreTestFetch = (registriesArg: Record<string, IAppStoreTestRegistryStub>): typeof fetch =>
|
|
208
|
+
async (inputArg: RequestInfo | URL, initArg?: RequestInit): Promise<Response> => {
|
|
209
|
+
const url = new URL(inputArg instanceof Request ? inputArg.url : inputArg);
|
|
210
|
+
const stub = registriesArg[url.origin];
|
|
211
|
+
if (stub === undefined) throw new TypeError(`fetch failed: no registry at ${url.origin}`);
|
|
212
|
+
const signal = initArg?.signal ?? null;
|
|
213
|
+
if (stub.hang === true) {
|
|
214
|
+
return await new Promise<Response>((_resolveArg, rejectArg) => {
|
|
215
|
+
const abort = () => rejectArg(signal?.reason ?? new Error('aborted'));
|
|
216
|
+
if (signal?.aborted === true) abort();
|
|
217
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (stub.status !== undefined) return new Response('unavailable', { status: stub.status });
|
|
221
|
+
if (url.pathname === `/${appStoreCatalogPackageName.replace('/', '%2f')}/latest`) {
|
|
222
|
+
return stub.manifest === null
|
|
223
|
+
? new Response('not found', { status: 404 })
|
|
224
|
+
: new Response(JSON.stringify(stub.manifest), { headers: { 'content-type': 'application/json' } });
|
|
225
|
+
}
|
|
226
|
+
if (url.pathname === tarballPath && stub.tarball !== undefined) {
|
|
227
|
+
return new Response(new Blob([stub.tarball]));
|
|
228
|
+
}
|
|
229
|
+
return new Response('not found', { status: 404 });
|
|
230
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './fixtures.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// node native
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export { crypto };
|
|
5
|
+
|
|
6
|
+
// @serve.zone scope
|
|
7
|
+
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
8
|
+
import * as servezoneInterfacesRuntime from '@serve.zone/interfaces/runtime';
|
|
9
|
+
|
|
10
|
+
export { servezoneInterfaces, servezoneInterfacesRuntime };
|
|
11
|
+
|
|
12
|
+
// third party
|
|
13
|
+
import * as modernTar from 'modern-tar';
|
|
14
|
+
|
|
15
|
+
export { modernTar };
|
package/apps/adminer/app.json
DELETED
package/apps/cloudly/app.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "cloudly",
|
|
3
|
-
"name": "Cloudly",
|
|
4
|
-
"description": "Multi-node serve.zone control plane for clusters, workload services, domains, and deployments.",
|
|
5
|
-
"category": "Dev Tools",
|
|
6
|
-
"iconName": "server",
|
|
7
|
-
"latestVersion": "1.27.0",
|
|
8
|
-
"versions": ["1.0.0", "1.1.0", "1.1.1", "1.1.2", "1.1.3", "1.1.4", "1.1.5", "1.1.6", "1.1.7", "1.1.8", "1.1.9", "1.1.10", "1.2.0", "1.2.1", "1.3.0", "1.4.0", "1.5.0", "1.6.0", "1.7.0", "1.8.0", "1.9.0", "1.10.0", "1.11.0", "1.12.0", "1.13.0", "1.14.0", "1.15.0", "1.16.0", "1.17.0", "1.18.0", "1.19.0", "1.20.0", "1.21.0", "1.22.0", "1.23.0", "1.24.0", "1.25.0", "1.26.0", "1.27.0"],
|
|
9
|
-
"maintainer": "serve.zone",
|
|
10
|
-
"links": {
|
|
11
|
-
"Source": "https://code.foss.global/serve.zone/cloudly",
|
|
12
|
-
"Docs": "https://serve.zone"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"image": "code.foss.global/serve.zone/cloudly:latest",
|
|
3
|
-
"targetPorts": [
|
|
4
|
-
{ "name": "web", "port": 3000, "protocol": "http", "default": true }
|
|
5
|
-
],
|
|
6
|
-
"envVars": [
|
|
7
|
-
{
|
|
8
|
-
"key": "SERVEZONE_ENVIRONMENT",
|
|
9
|
-
"value": "production",
|
|
10
|
-
"description": "Cloudly runtime environment.",
|
|
11
|
-
"required": true
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"key": "SERVEZONE_PUBLIC_ORIGIN",
|
|
15
|
-
"value": "https://${SERVICE_DOMAIN}",
|
|
16
|
-
"description": "Canonical public Cloudly origin including protocol and optional public port.",
|
|
17
|
-
"required": true
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"key": "SERVEZONE_LISTENPORT",
|
|
21
|
-
"value": "3000",
|
|
22
|
-
"description": "Internal Cloudly HTTP port inside the container.",
|
|
23
|
-
"required": true
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"key": "SERVEZONE_SSLMODE",
|
|
27
|
-
"value": "external",
|
|
28
|
-
"description": "Use external TLS termination through Onebox or dcrouter.",
|
|
29
|
-
"required": true
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"key": "MONGODB_URL",
|
|
33
|
-
"value": "${MONGODB_URI}",
|
|
34
|
-
"description": "Authenticated MongoDB connection URL provisioned by Onebox.",
|
|
35
|
-
"required": true
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"key": "MONGODB_NAME",
|
|
39
|
-
"value": "${MONGODB_DATABASE}",
|
|
40
|
-
"description": "MongoDB database name provisioned by Onebox.",
|
|
41
|
-
"required": true
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"key": "MONGODB_USER",
|
|
45
|
-
"value": "${MONGODB_USERNAME}",
|
|
46
|
-
"description": "MongoDB username provisioned by Onebox.",
|
|
47
|
-
"required": true
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"key": "MONGODB_PASS",
|
|
51
|
-
"value": "${MONGODB_PASSWORD}",
|
|
52
|
-
"description": "MongoDB password provisioned by Onebox.",
|
|
53
|
-
"required": true
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"key": "S3_ENDPOINT",
|
|
57
|
-
"value": "onebox-minio",
|
|
58
|
-
"description": "S3 endpoint host for the MinIO service provisioned by Onebox.",
|
|
59
|
-
"required": true
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"key": "S3_PORT",
|
|
63
|
-
"value": "9000",
|
|
64
|
-
"description": "S3 endpoint port for the MinIO service provisioned by Onebox.",
|
|
65
|
-
"required": true
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"key": "S3_USESSL",
|
|
69
|
-
"value": "false",
|
|
70
|
-
"description": "Use plain HTTP for internal MinIO traffic on the Onebox network.",
|
|
71
|
-
"required": true
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
"key": "S3_BUCKET",
|
|
75
|
-
"value": "${S3_BUCKET}",
|
|
76
|
-
"description": "S3 bucket provisioned by Onebox for Cloudly's registry.",
|
|
77
|
-
"required": true
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"key": "S3_ACCESSKEY",
|
|
81
|
-
"value": "${S3_ACCESS_KEY}",
|
|
82
|
-
"description": "S3 access key provisioned by Onebox.",
|
|
83
|
-
"required": true
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"key": "S3_SECRETKEY",
|
|
87
|
-
"value": "${S3_SECRET_KEY}",
|
|
88
|
-
"description": "S3 secret key provisioned by Onebox.",
|
|
89
|
-
"required": true
|
|
90
|
-
}
|
|
91
|
-
],
|
|
92
|
-
"platformRequirements": {
|
|
93
|
-
"mongodb": true,
|
|
94
|
-
"s3": true
|
|
95
|
-
},
|
|
96
|
-
"platformOidc": {
|
|
97
|
-
"redirectPath": "/auth/oidc/callback",
|
|
98
|
-
"roles": [
|
|
99
|
-
{
|
|
100
|
-
"id": "admin",
|
|
101
|
-
"label": "Administrator",
|
|
102
|
-
"description": "Full Cloudly administration for this hosted instance."
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"id": "user",
|
|
106
|
-
"label": "User",
|
|
107
|
-
"description": "Standard authenticated Cloudly access for this hosted instance."
|
|
108
|
-
}
|
|
109
|
-
],
|
|
110
|
-
"environmentVariables": {
|
|
111
|
-
"issuerUrl": "SERVEZONE_PLATFORM_OIDC_ISSUER",
|
|
112
|
-
"clientId": "SERVEZONE_PLATFORM_OIDC_CLIENT_ID",
|
|
113
|
-
"clientSecret": "SERVEZONE_PLATFORM_OIDC_CLIENT_SECRET",
|
|
114
|
-
"redirectUri": "SERVEZONE_PLATFORM_OIDC_REDIRECT_URI",
|
|
115
|
-
"audience": "SERVEZONE_PLATFORM_OIDC_AUDIENCE"
|
|
116
|
-
},
|
|
117
|
-
"clientAuthenticationMethod": "client_secret_basic"
|
|
118
|
-
},
|
|
119
|
-
"minOneboxVersion": "2.2.0"
|
|
120
|
-
}
|