@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.
Files changed (125) hide show
  1. package/.smartconfig.json +4 -1
  2. package/changelog.md +70 -0
  3. package/dist_catalog/catalog.digest +1 -0
  4. package/dist_catalog/catalog.envelope.json +7617 -0
  5. package/{dist_ts_client/data.generated.js → dist_catalog/catalog.json} +6912 -6908
  6. package/dist_ts_client/catalog.d.ts +16 -0
  7. package/dist_ts_client/catalog.js +12 -0
  8. package/dist_ts_client/index.d.ts +3 -1
  9. package/dist_ts_client/index.js +4 -2
  10. package/dist_ts_client/loader.d.ts +69 -0
  11. package/dist_ts_client/loader.js +361 -0
  12. package/dist_ts_client/plugins.d.ts +5 -0
  13. package/dist_ts_client/plugins.js +8 -0
  14. package/dist_ts_client/projection.d.ts +59 -0
  15. package/dist_ts_client/projection.js +83 -0
  16. package/dist_ts_client/storage.d.ts +15 -9
  17. package/dist_ts_client/storage.js +81 -657
  18. package/dist_ts_client/types.d.ts +86 -41
  19. package/dist_ts_client/types.js +1 -1
  20. package/dist_ts_testing/fixtures.d.ts +83 -0
  21. package/dist_ts_testing/fixtures.js +146 -0
  22. package/dist_ts_testing/index.d.ts +1 -0
  23. package/dist_ts_testing/index.js +2 -0
  24. package/dist_ts_testing/plugins.d.ts +7 -0
  25. package/dist_ts_testing/plugins.js +11 -0
  26. package/package.json +16 -11
  27. package/readme.md +162 -93
  28. package/ts_client/catalog.ts +26 -0
  29. package/ts_client/index.ts +3 -1
  30. package/ts_client/loader.ts +414 -0
  31. package/ts_client/plugins.ts +10 -0
  32. package/ts_client/projection.ts +148 -0
  33. package/ts_client/readme.md +17 -27
  34. package/ts_client/storage.ts +93 -882
  35. package/ts_client/types.ts +102 -42
  36. package/ts_testing/fixtures.ts +230 -0
  37. package/ts_testing/index.ts +1 -0
  38. package/ts_testing/plugins.ts +15 -0
  39. package/ts_testing/tspublish.json +3 -0
  40. package/apps/adminer/app.json +0 -10
  41. package/apps/adminer/versions/1.0.0/config.json +0 -6
  42. package/apps/cloudly/app.json +0 -14
  43. package/apps/cloudly/versions/1.0.0/config.json +0 -120
  44. package/apps/cloudly/versions/1.1.0/config.json +0 -120
  45. package/apps/cloudly/versions/1.1.1/config.json +0 -120
  46. package/apps/cloudly/versions/1.1.10/config.json +0 -173
  47. package/apps/cloudly/versions/1.1.2/config.json +0 -130
  48. package/apps/cloudly/versions/1.1.3/config.json +0 -130
  49. package/apps/cloudly/versions/1.1.4/config.json +0 -130
  50. package/apps/cloudly/versions/1.1.5/config.json +0 -149
  51. package/apps/cloudly/versions/1.1.6/config.json +0 -149
  52. package/apps/cloudly/versions/1.1.7/config.json +0 -149
  53. package/apps/cloudly/versions/1.1.8/config.json +0 -173
  54. package/apps/cloudly/versions/1.1.9/config.json +0 -173
  55. package/apps/cloudly/versions/1.10.0/config.json +0 -173
  56. package/apps/cloudly/versions/1.11.0/config.json +0 -173
  57. package/apps/cloudly/versions/1.12.0/config.json +0 -173
  58. package/apps/cloudly/versions/1.13.0/config.json +0 -173
  59. package/apps/cloudly/versions/1.14.0/config.json +0 -173
  60. package/apps/cloudly/versions/1.15.0/config.json +0 -173
  61. package/apps/cloudly/versions/1.16.0/config.json +0 -173
  62. package/apps/cloudly/versions/1.17.0/config.json +0 -173
  63. package/apps/cloudly/versions/1.18.0/config.json +0 -173
  64. package/apps/cloudly/versions/1.19.0/config.json +0 -173
  65. package/apps/cloudly/versions/1.2.0/config.json +0 -173
  66. package/apps/cloudly/versions/1.2.1/config.json +0 -173
  67. package/apps/cloudly/versions/1.20.0/config.json +0 -173
  68. package/apps/cloudly/versions/1.21.0/config.json +0 -173
  69. package/apps/cloudly/versions/1.22.0/config.json +0 -173
  70. package/apps/cloudly/versions/1.23.0/config.json +0 -173
  71. package/apps/cloudly/versions/1.24.0/config.json +0 -173
  72. package/apps/cloudly/versions/1.25.0/config.json +0 -173
  73. package/apps/cloudly/versions/1.26.0/config.json +0 -173
  74. package/apps/cloudly/versions/1.27.0/config.json +0 -173
  75. package/apps/cloudly/versions/1.3.0/config.json +0 -173
  76. package/apps/cloudly/versions/1.4.0/config.json +0 -173
  77. package/apps/cloudly/versions/1.5.0/config.json +0 -173
  78. package/apps/cloudly/versions/1.6.0/config.json +0 -173
  79. package/apps/cloudly/versions/1.7.0/config.json +0 -173
  80. package/apps/cloudly/versions/1.8.0/config.json +0 -173
  81. package/apps/cloudly/versions/1.9.0/config.json +0 -173
  82. package/apps/ghost/app.json +0 -10
  83. package/apps/ghost/versions/1.0.0/config.json +0 -18
  84. package/apps/gitea/app.json +0 -10
  85. package/apps/gitea/versions/1.0.0/config.json +0 -16
  86. package/apps/gitops/app.json +0 -14
  87. package/apps/gitops/versions/1.0.0/config.json +0 -28
  88. package/apps/grafana/app.json +0 -10
  89. package/apps/grafana/versions/1.0.0/config.json +0 -9
  90. package/apps/mariadb/app.json +0 -10
  91. package/apps/mariadb/versions/1.0.0/config.json +0 -9
  92. package/apps/mattermost/app.json +0 -10
  93. package/apps/mattermost/versions/1.0.0/config.json +0 -6
  94. package/apps/n8n/app.json +0 -10
  95. package/apps/n8n/versions/1.0.0/config.json +0 -6
  96. package/apps/nextcloud/app.json +0 -10
  97. package/apps/nextcloud/versions/1.0.0/config.json +0 -16
  98. package/apps/nginx/app.json +0 -10
  99. package/apps/nginx/versions/1.0.0/config.json +0 -6
  100. package/apps/openbao/app.json +0 -16
  101. package/apps/openbao/versions/1.0.0/config.json +0 -52
  102. package/apps/plausible/app.json +0 -10
  103. package/apps/plausible/versions/1.0.0/config.json +0 -9
  104. package/apps/portainer/app.json +0 -10
  105. package/apps/portainer/versions/1.0.0/config.json +0 -6
  106. package/apps/postgres/app.json +0 -10
  107. package/apps/postgres/versions/1.0.0/config.json +0 -11
  108. package/apps/rustdesk-server/app.json +0 -14
  109. package/apps/rustdesk-server/versions/1.0.0/config.json +0 -19
  110. package/apps/siprouter/app.json +0 -14
  111. package/apps/siprouter/versions/1.0.0/config.json +0 -84
  112. package/apps/uptime-kuma/app.json +0 -10
  113. package/apps/uptime-kuma/versions/1.0.0/config.json +0 -6
  114. package/apps/valkey/app.json +0 -10
  115. package/apps/valkey/versions/1.0.0/config.json +0 -6
  116. package/apps/vaultwarden/app.json +0 -10
  117. package/apps/vaultwarden/versions/1.0.0/config.json +0 -6
  118. package/apps/wordpress/app.json +0 -10
  119. package/apps/wordpress/versions/1.0.0/config.json +0 -15
  120. package/appstore.json +0 -194
  121. package/dist_ts_client/classes.appstoreresolver.d.ts +0 -59
  122. package/dist_ts_client/classes.appstoreresolver.js +0 -736
  123. package/dist_ts_client/data.generated.d.ts +0 -2
  124. package/ts_client/classes.appstoreresolver.ts +0 -896
  125. package/ts_client/data.generated.ts +0 -7606
@@ -1,896 +0,0 @@
1
- import * as interfaces from '@serve.zone/interfaces';
2
- import { SmartVersion } from '@push.rocks/smartversion';
3
- import packageJson from '../package.json' with { type: 'json' };
4
- import { appStorePackageData } from './data.generated.js';
5
- import {
6
- normalizeAppStoreStorageConfig,
7
- validateAppStoreStorageConfig,
8
- } from './storage.js';
9
- import type {
10
- IAppStoreResolverOptions,
11
- IAppStoreSourceInfo,
12
- IAppStoreStorageNormalizationOptions,
13
- INormalizedAppStoreStorageConfig,
14
- IParsedDockerImageReference,
15
- IResolvedAppStoreApp,
16
- TAppStoreApp,
17
- TAppStoreDockerImageSource,
18
- TAppStoreIndex,
19
- TAppStoreRepoManifestSource,
20
- TAppStoreResolvedSource,
21
- TAppStoreSourceMode,
22
- TAppStoreVersionConfig,
23
- TServezoneAppStoreManifest,
24
- } from './types.js';
25
-
26
- export const appStorePackageMetadata = {
27
- name: packageJson.name,
28
- version: packageJson.version,
29
- };
30
-
31
- export const appStoreDefaultRemoteBaseUrl = 'https://code.foss.global/serve.zone/appstore/raw/branch/main';
32
-
33
- export class AppStoreResolver {
34
- public readonly baseUrl: string;
35
- private readonly fetchRef: typeof fetch;
36
- private readonly source: TAppStoreSourceMode;
37
- private readonly resolveDockerDigests: boolean;
38
- private readonly requestTimeoutMs: number;
39
- private readonly now: () => Date;
40
- private appStoreCache: TAppStoreIndex | null = null;
41
- private inlineAppMetaCache = new Map<string, interfaces.appstore.IAppStoreAppMeta>();
42
- private sourceAppCache = new Map<string, IResolvedAppStoreApp>();
43
-
44
- constructor(optionsArg: IAppStoreResolverOptions = {}) {
45
- this.source = optionsArg.source || (optionsArg.baseUrl || optionsArg.fetch ? 'remote' : 'package');
46
- this.baseUrl = optionsArg.baseUrl || appStoreDefaultRemoteBaseUrl;
47
- this.fetchRef = optionsArg.fetch || fetch;
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
- }
53
- this.now = optionsArg.now || (() => new Date());
54
- }
55
-
56
- public getSourceInfo(): IAppStoreSourceInfo {
57
- return {
58
- source: this.source,
59
- packageName: appStorePackageMetadata.name,
60
- packageVersion: appStorePackageMetadata.version,
61
- ...(this.source === 'remote' ? { baseUrl: this.baseUrl } : {}),
62
- };
63
- }
64
-
65
- public async getAppStoreIndex(): Promise<TAppStoreIndex> {
66
- if (this.appStoreCache) {
67
- return this.appStoreCache;
68
- }
69
- const appStore = await this.fetchAppStoreIndex();
70
- this.appStoreCache = await this.resolveAppStore(appStore);
71
- return this.appStoreCache;
72
- }
73
-
74
- public async getApps(): Promise<TAppStoreApp[]> {
75
- return (await this.getAppStoreIndex()).apps;
76
- }
77
-
78
- public async getAppMeta(appIdArg: string): Promise<interfaces.appstore.IAppStoreAppMeta> {
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
- }
83
- if (app?.source?.type === 'repoManifest') {
84
- return (await this.resolveRepoManifestSource(app.source, app)).appMeta;
85
- }
86
- if (app?.source?.type === 'dockerImage') {
87
- return this.createAppMetaFromAppStoreApp(app);
88
- }
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);
94
- }
95
-
96
- public async getAppVersionConfig(
97
- appIdArg: string,
98
- versionArg: string,
99
- ): Promise<TAppStoreVersionConfig> {
100
- const app = await this.getAppStoreApp(appIdArg);
101
- if (app?.source?.type === 'repoManifest') {
102
- const resolvedApp = await this.resolveRepoManifestSource(app.source, app);
103
- const config = resolvedApp.configsByVersion.get(versionArg);
104
- if (!config) {
105
- throw new Error(`Version '${versionArg}' is not defined by the linked appstore manifest`);
106
- }
107
- this.validateAppStoreVersionConfig(config, `${appIdArg}@${versionArg}`);
108
- return config;
109
- }
110
-
111
- if (app?.source?.type === 'dockerImage' && app.runtime) {
112
- const config: TAppStoreVersionConfig = { ...app.runtime };
113
- await this.applyDockerImageSourceToConfig(app.source, config, versionArg);
114
- this.validateAppStoreVersionConfig(config, `${appIdArg}@${versionArg}`);
115
- return config;
116
- }
117
-
118
- let config: TAppStoreVersionConfig;
119
- try {
120
- config = await this.fetchJson(`apps/${appIdArg}/versions/${versionArg}/config.json`) as TAppStoreVersionConfig;
121
- } catch (error) {
122
- if (app?.source?.type !== 'dockerImage') {
123
- throw error;
124
- }
125
- const appMeta = await this.fetchJson(`apps/${appIdArg}/app.json`) as interfaces.appstore.IAppStoreAppMeta;
126
- config = await this.fetchJson(`apps/${appIdArg}/versions/${appMeta.latestVersion}/config.json`) as TAppStoreVersionConfig;
127
- }
128
-
129
- if (app?.source?.type === 'dockerImage') {
130
- await this.applyDockerImageSourceToConfig(app.source, config, versionArg);
131
- }
132
- this.validateAppStoreVersionConfig(config, `${appIdArg}@${versionArg}`);
133
- return config;
134
- }
135
-
136
- public async resolveAppStore(appStoreArg: unknown): Promise<TAppStoreIndex> {
137
- const appStore = parseAppStoreIndex(appStoreArg);
138
- this.sourceAppCache.clear();
139
- this.inlineAppMetaCache.clear();
140
- const apps: TAppStoreApp[] = [];
141
-
142
- for (const app of appStore.apps) {
143
- apps.push(await this.resolveAppStoreApp(app));
144
- }
145
-
146
- return {
147
- ...appStore,
148
- apps,
149
- resolvedAt: this.now().toISOString(),
150
- };
151
- }
152
-
153
- public async resolveRepoManifestSource(
154
- sourceArg: TAppStoreRepoManifestSource,
155
- appArg?: TAppStoreApp,
156
- ): Promise<IResolvedAppStoreApp> {
157
- const cacheKey = `${sourceArg.url}#${sourceArg.ref || ''}`;
158
- const cachedApp = this.sourceAppCache.get(cacheKey);
159
- if (cachedApp) {
160
- return cachedApp;
161
- }
162
-
163
- const manifestText = await this.fetchTextFromUrl(sourceArg.url);
164
- const manifestHash = await createSha256Hex(manifestText);
165
- const manifest = parseServezoneAppStoreManifest(JSON.parse(manifestText));
166
- const resolvedApp = await this.resolveServezoneAppStoreManifest(manifest, {
167
- type: 'repoManifest',
168
- url: sourceArg.url,
169
- ref: sourceArg.ref,
170
- manifestHash,
171
- resolvedAt: this.now().toISOString(),
172
- });
173
-
174
- if (appArg) {
175
- resolvedApp.appStoreApp = {
176
- ...resolvedApp.appStoreApp,
177
- ...withoutUndefined(appArg),
178
- latestVersion: resolvedApp.appStoreApp.latestVersion,
179
- versions: resolvedApp.appStoreApp.versions,
180
- source: appArg.source,
181
- tags: appArg.tags || resolvedApp.appStoreApp.tags,
182
- resolvedSource: resolvedApp.appStoreApp.resolvedSource,
183
- };
184
- resolvedApp.appMeta = {
185
- ...resolvedApp.appMeta,
186
- id: resolvedApp.appStoreApp.id,
187
- name: resolvedApp.appStoreApp.name,
188
- description: resolvedApp.appStoreApp.description,
189
- category: resolvedApp.appStoreApp.category,
190
- iconName: resolvedApp.appStoreApp.iconName,
191
- latestVersion: resolvedApp.appStoreApp.latestVersion,
192
- versions: resolvedApp.appStoreApp.versions || resolvedApp.appMeta.versions,
193
- tags: resolvedApp.appStoreApp.tags,
194
- source: appArg.source,
195
- resolvedSource: resolvedApp.appStoreApp.resolvedSource,
196
- };
197
- }
198
-
199
- this.sourceAppCache.set(cacheKey, resolvedApp);
200
- return resolvedApp;
201
- }
202
-
203
- public async resolveDockerImageSource(
204
- sourceArg: TAppStoreDockerImageSource,
205
- ): Promise<TAppStoreResolvedSource> {
206
- let imageDigest: string | undefined;
207
- if (sourceArg.tracking === 'digest' && this.resolveDockerDigests) {
208
- imageDigest = await this.resolveDockerImageDigest(sourceArg.image) || undefined;
209
- }
210
-
211
- return {
212
- type: 'dockerImage',
213
- image: sourceArg.image,
214
- imageDigest,
215
- resolvedAt: this.now().toISOString(),
216
- };
217
- }
218
-
219
- public async resolveDockerImageDigest(imageArg: string): Promise<string | null> {
220
- const parsedImage = parseDockerImageReference(imageArg);
221
- if (parsedImage.digest) {
222
- return parsedImage.digest;
223
- }
224
- return await this.fetchDockerManifestDigest(parsedImage);
225
- }
226
-
227
- public validateAppStoreVersionConfig(
228
- configArg: TAppStoreVersionConfig,
229
- labelArg = 'appstore config',
230
- ): void {
231
- if (!configArg || typeof configArg !== 'object') {
232
- throw new Error(`Invalid ${labelArg}: config must be an object`);
233
- }
234
- if (!configArg.image || typeof configArg.image !== 'string') {
235
- throw new Error(`Invalid ${labelArg}: image is required`);
236
- }
237
- if (configArg.port !== undefined) {
238
- this.assertValidPort(configArg.port, `${labelArg} port`);
239
- }
240
- this.validateTargetPortModel(configArg, labelArg);
241
- const containerArgErrors = interfaces.data.validateServiceContainerArgs(configArg.containerArgs);
242
- if (containerArgErrors.length > 0) {
243
- throw new Error(`Invalid ${labelArg}: ${containerArgErrors.join('; ')}`);
244
- }
245
-
246
- const environmentErrors = interfaces.appstore.validateAppStoreEnvironmentDeclarations(
247
- configArg.envVars,
248
- );
249
- if (environmentErrors.length > 0) {
250
- throw new Error(`Invalid ${labelArg}: ${environmentErrors.join('; ')}`);
251
- }
252
-
253
- validateAppStoreStorageConfig(configArg, labelArg);
254
- const platformOidcErrors = interfaces.appstore.validateAppStoreVersionPlatformOidc(configArg);
255
- if (platformOidcErrors.length > 0) {
256
- throw new Error(`Invalid ${labelArg}: ${platformOidcErrors.join('; ')}`);
257
- }
258
- this.validatePublishedPorts(configArg.publishedPorts || [], labelArg);
259
- }
260
-
261
- /**
262
- * Produces the canonical storage handoff only after target feature
263
- * negotiation. Fulfillment adapters should use this method before creating
264
- * or mutating any storage resource.
265
- */
266
- public normalizeStorageConfig(
267
- configArg: TAppStoreVersionConfig,
268
- optionsArg: IAppStoreStorageNormalizationOptions,
269
- labelArg = 'appstore config',
270
- ): INormalizedAppStoreStorageConfig {
271
- this.validateAppStoreVersionConfig(configArg, labelArg);
272
- return normalizeAppStoreStorageConfig(configArg, optionsArg, labelArg);
273
- }
274
-
275
- public normalizeVolumes(
276
- volumesArg: TAppStoreVersionConfig['volumes'] = [],
277
- ): interfaces.appstore.IAppStoreVolume[] {
278
- return volumesArg.map((volumeArg): interfaces.appstore.IAppStoreVolume => {
279
- if (typeof volumeArg === 'string') {
280
- return { mountPath: volumeArg };
281
- }
282
- return volumeArg;
283
- }).map((volumeArg, indexArg) => {
284
- this.validateVolume(volumeArg, `volume ${indexArg + 1}`);
285
- return volumeArg;
286
- });
287
- }
288
-
289
- private async fetchAppStoreIndex(): Promise<TAppStoreIndex> {
290
- try {
291
- return await this.fetchJson('appstore.resolved.json') as TAppStoreIndex;
292
- } catch {
293
- return await this.fetchJson('appstore.json') as TAppStoreIndex;
294
- }
295
- }
296
-
297
- private async resolveAppStoreApp(appArg: TAppStoreApp): Promise<TAppStoreApp> {
298
- if (appArg.source?.type === 'repoManifest') {
299
- const resolvedApp = await this.resolveRepoManifestSource(appArg.source, appArg);
300
- return {
301
- ...resolvedApp.appStoreApp,
302
- ...withoutUndefined(appArg),
303
- latestVersion: resolvedApp.appStoreApp.latestVersion,
304
- versions: resolvedApp.appStoreApp.versions,
305
- source: appArg.source,
306
- tags: appArg.tags || resolvedApp.appStoreApp.tags,
307
- resolvedSource: resolvedApp.appStoreApp.resolvedSource,
308
- };
309
- }
310
-
311
- if (appArg.source?.type === 'dockerImage') {
312
- const config = appArg.runtime ? { ...appArg.runtime } : undefined;
313
- const resolvedSource = config
314
- ? (await this.applyDockerImageSourceToConfig(appArg.source, config, appArg.latestVersion)).resolvedSource
315
- : await this.resolveDockerImageSource(appArg.source);
316
- const latestVersion = createAppStoreVersionForDockerSource(
317
- appArg.source,
318
- appArg.latestVersion,
319
- resolvedSource?.imageDigest,
320
- );
321
- return {
322
- ...appArg,
323
- runtime: config,
324
- latestVersion,
325
- versions: uniqueStrings([...(appArg.versions || []), latestVersion]),
326
- upgradeStrategy: appArg.source.tracking === 'digest' ? 'dockerDigest' : appArg.upgradeStrategy,
327
- resolvedSource,
328
- };
329
- }
330
-
331
- const appMeta = await this.fetchJson(
332
- `apps/${appArg.id}/app.json`,
333
- ) as interfaces.appstore.IAppStoreAppMeta;
334
- return this.validateInlineAppMetadata(appArg, appMeta);
335
- }
336
-
337
- private async resolveServezoneAppStoreManifest(
338
- manifestArg: TServezoneAppStoreManifest,
339
- resolvedSourceArg: TAppStoreResolvedSource,
340
- ): Promise<IResolvedAppStoreApp> {
341
- const configsByVersion = new Map<string, TAppStoreVersionConfig>();
342
- const versions: string[] = [];
343
- const sourceVersionToResolvedVersion = new Map<string, string>();
344
-
345
- for (const versionArg of manifestArg.versions || []) {
346
- const sourceVersion = versionArg.version;
347
- const { version: _version, ...versionConfig } = versionArg;
348
- const config: TAppStoreVersionConfig = {
349
- ...versionConfig,
350
- source: versionConfig.source || manifestArg.source,
351
- resolvedSource: resolvedSourceArg,
352
- };
353
- await this.resolveConfigSource(config, sourceVersion);
354
- const resolvedVersion = config.appStoreVersion || sourceVersion;
355
- config.appStoreVersion = resolvedVersion;
356
- this.validateAppStoreVersionConfig(config, `${manifestArg.app.id}@${resolvedVersion}`);
357
- configsByVersion.set(resolvedVersion, config);
358
- configsByVersion.set(sourceVersion, config);
359
- versions.push(resolvedVersion);
360
- sourceVersionToResolvedVersion.set(sourceVersion, resolvedVersion);
361
- }
362
-
363
- if (manifestArg.runtime) {
364
- const sourceVersion = manifestArg.latestVersion || manifestArg.channel || 'latest';
365
- const config: TAppStoreVersionConfig = {
366
- ...manifestArg.runtime,
367
- source: manifestArg.runtime.source || manifestArg.source,
368
- resolvedSource: resolvedSourceArg,
369
- };
370
- await this.resolveConfigSource(config, sourceVersion);
371
- const resolvedVersion = config.appStoreVersion || sourceVersion;
372
- config.appStoreVersion = resolvedVersion;
373
- this.validateAppStoreVersionConfig(config, `${manifestArg.app.id}@${resolvedVersion}`);
374
- configsByVersion.set(resolvedVersion, config);
375
- configsByVersion.set(sourceVersion, config);
376
- versions.push(resolvedVersion);
377
- sourceVersionToResolvedVersion.set(sourceVersion, resolvedVersion);
378
- }
379
-
380
- if (configsByVersion.size === 0) {
381
- throw new Error('Appstore manifest must define at least one runtime config or version');
382
- }
383
-
384
- const selectedChannel = manifestArg.policy?.defaultChannel || manifestArg.channel || 'stable';
385
- const channelVersion = manifestArg.channels?.[selectedChannel];
386
- const uniqueVersions = uniqueStrings(versions);
387
- const declaredLatestVersion = manifestArg.latestVersion
388
- || channelVersion
389
- || selectImplicitLatestVersion(uniqueVersions, `Appstore manifest '${manifestArg.app.id}'`);
390
- const latestVersion = sourceVersionToResolvedVersion.get(declaredLatestVersion)
391
- || declaredLatestVersion;
392
- if (!configsByVersion.has(latestVersion)) {
393
- throw new Error(
394
- `Appstore manifest '${manifestArg.app.id}' latestVersion '${declaredLatestVersion}' has no runtime config`,
395
- );
396
- }
397
-
398
- const appStoreApp: TAppStoreApp = {
399
- id: manifestArg.app.id,
400
- name: manifestArg.app.name,
401
- description: manifestArg.app.description,
402
- category: manifestArg.app.category,
403
- iconName: manifestArg.app.iconName,
404
- iconUrl: manifestArg.app.iconUrl,
405
- latestVersion,
406
- versions: uniqueVersions,
407
- tags: manifestArg.app.tags,
408
- channel: selectedChannel,
409
- source: manifestArg.source,
410
- upgradeStrategy: getUpgradeStrategyForConfig(configsByVersion.get(latestVersion)),
411
- resolvedSource: resolvedSourceArg,
412
- };
413
-
414
- const appMeta: interfaces.appstore.IAppStoreAppMeta = {
415
- id: manifestArg.app.id,
416
- name: manifestArg.app.name,
417
- description: manifestArg.app.description,
418
- category: manifestArg.app.category,
419
- iconName: manifestArg.app.iconName,
420
- latestVersion,
421
- versions: uniqueVersions,
422
- maintainer: manifestArg.app.maintainer,
423
- links: manifestArg.app.links,
424
- tags: manifestArg.app.tags,
425
- source: manifestArg.source,
426
- resolvedSource: resolvedSourceArg,
427
- };
428
-
429
- return { appStoreApp, appMeta, configsByVersion };
430
- }
431
-
432
- private async resolveConfigSource(configArg: TAppStoreVersionConfig, versionArg: string): Promise<void> {
433
- if (configArg.source?.type === 'dockerImage') {
434
- await this.applyDockerImageSourceToConfig(configArg.source, configArg, versionArg);
435
- }
436
- }
437
-
438
- private async applyDockerImageSourceToConfig(
439
- sourceArg: TAppStoreDockerImageSource,
440
- configArg: TAppStoreVersionConfig,
441
- versionArg: string,
442
- ): Promise<TAppStoreVersionConfig> {
443
- configArg.image = sourceArg.image;
444
- configArg.source = sourceArg;
445
-
446
- const resolvedSource = await this.resolveDockerImageSource(sourceArg);
447
- configArg.resolvedSource = resolvedSource;
448
- configArg.resolvedImageDigest = resolvedSource.imageDigest;
449
- configArg.upgradeStrategy = sourceArg.tracking === 'digest' ? 'dockerDigest' : configArg.upgradeStrategy;
450
- configArg.appStoreVersion = createAppStoreVersionForDockerSource(
451
- sourceArg,
452
- versionArg,
453
- resolvedSource.imageDigest,
454
- );
455
-
456
- return configArg;
457
- }
458
-
459
- private validateInlineAppMetadata(
460
- appArg: TAppStoreApp,
461
- appMetaArg: interfaces.appstore.IAppStoreAppMeta,
462
- ): TAppStoreApp {
463
- if (appMetaArg.id !== appArg.id) {
464
- throw new Error(
465
- `App Store metadata ID '${appMetaArg.id}' does not match index app '${appArg.id}'`,
466
- );
467
- }
468
- if (!appMetaArg.latestVersion) {
469
- throw new Error(`App Store metadata for '${appArg.id}' has no latestVersion`);
470
- }
471
- const versions = uniqueStrings(
472
- appMetaArg.versions?.length ? appMetaArg.versions : [appMetaArg.latestVersion],
473
- );
474
- if (!versions.includes(appMetaArg.latestVersion)) {
475
- throw new Error(
476
- `App Store metadata for '${appArg.id}' latestVersion '${appMetaArg.latestVersion}' is not declared in versions`,
477
- );
478
- }
479
- const semverLatest = selectHighestSemanticVersion(versions);
480
- if (semverLatest && semverLatest !== appMetaArg.latestVersion) {
481
- throw new Error(
482
- `App Store metadata for '${appArg.id}' declares latestVersion '${appMetaArg.latestVersion}' but highest semantic version is '${semverLatest}'`,
483
- );
484
- }
485
- const resolvedLatestVersion = semverLatest || appMetaArg.latestVersion;
486
- if (appArg.latestVersion !== resolvedLatestVersion) {
487
- throw new Error(
488
- `App Store index for '${appArg.id}' declares latestVersion '${appArg.latestVersion}' but app metadata resolves '${resolvedLatestVersion}'`,
489
- );
490
- }
491
- this.inlineAppMetaCache.set(appArg.id, {
492
- ...appMetaArg,
493
- latestVersion: resolvedLatestVersion,
494
- versions,
495
- });
496
- return {
497
- ...appArg,
498
- latestVersion: resolvedLatestVersion,
499
- versions,
500
- };
501
- }
502
-
503
- private createAppMetaFromAppStoreApp(appArg: TAppStoreApp): interfaces.appstore.IAppStoreAppMeta {
504
- return {
505
- id: appArg.id,
506
- name: appArg.name,
507
- description: appArg.description,
508
- category: appArg.category,
509
- iconName: appArg.iconName,
510
- latestVersion: appArg.latestVersion,
511
- versions: appArg.versions || [appArg.latestVersion],
512
- tags: appArg.tags,
513
- source: appArg.source,
514
- resolvedSource: appArg.resolvedSource,
515
- };
516
- }
517
-
518
- private async getAppStoreApp(appIdArg: string): Promise<TAppStoreApp | undefined> {
519
- const appStore = await this.getAppStoreIndex();
520
- return appStore.apps.find((appArg) => appArg.id === appIdArg);
521
- }
522
-
523
- private async withRemoteResponse<T>(
524
- urlArg: string,
525
- initArg: RequestInit,
526
- consumeArg: (responseArg: Response) => Promise<T> | T,
527
- ): Promise<T> {
528
- const controller = new AbortController();
529
- const upstreamSignal = initArg.signal;
530
- const forwardAbort = (): void => {
531
- controller.abort(upstreamSignal?.reason);
532
- };
533
- if (upstreamSignal?.aborted) {
534
- forwardAbort();
535
- } else {
536
- upstreamSignal?.addEventListener('abort', forwardAbort, { once: true });
537
- }
538
- const timeout = setTimeout(() => {
539
- controller.abort(new Error(`App Store request timed out after ${this.requestTimeoutMs}ms: ${urlArg}`));
540
- }, this.requestTimeoutMs);
541
- let response: Response | undefined;
542
- try {
543
- response = await this.fetchRef(urlArg, {
544
- ...initArg,
545
- signal: controller.signal,
546
- });
547
- return await consumeArg(response);
548
- } catch (error) {
549
- if (controller.signal.aborted && controller.signal.reason instanceof Error) {
550
- throw controller.signal.reason;
551
- }
552
- throw error;
553
- } finally {
554
- clearTimeout(timeout);
555
- upstreamSignal?.removeEventListener('abort', forwardAbort);
556
- if (response?.body && !response.bodyUsed) {
557
- void response.body.cancel().catch(() => {});
558
- }
559
- }
560
- }
561
-
562
- private async fetchRemoteHeaders(
563
- urlArg: string,
564
- initArg: RequestInit = {},
565
- ): Promise<{ status: number; ok: boolean; headers: Headers }> {
566
- return await this.withRemoteResponse(urlArg, initArg, (responseArg) => ({
567
- status: responseArg.status,
568
- ok: responseArg.ok,
569
- headers: new Headers(responseArg.headers),
570
- }));
571
- }
572
-
573
- private async fetchJson(pathArg: string): Promise<unknown> {
574
- if (this.source === 'package') {
575
- const value = appStorePackageData.jsonFiles[pathArg];
576
- if (value === undefined) {
577
- throw new Error(`Packaged App Store file not found: ${pathArg}`);
578
- }
579
- return structuredClone(value);
580
- }
581
-
582
- const url = `${this.baseUrl}/${pathArg}`;
583
- return await this.withRemoteResponse(url, {}, async (responseArg) => {
584
- if (!responseArg.ok) {
585
- throw new Error(`HTTP ${responseArg.status} for ${url}`);
586
- }
587
- return await responseArg.json();
588
- });
589
- }
590
-
591
- public async fetchText(pathArg: string): Promise<string> {
592
- if (this.source === 'package') {
593
- if (pathArg in appStorePackageData.textFiles) {
594
- return appStorePackageData.textFiles[pathArg];
595
- }
596
- if (pathArg in appStorePackageData.jsonFiles) {
597
- return JSON.stringify(appStorePackageData.jsonFiles[pathArg]);
598
- }
599
- throw new Error(`Packaged App Store file not found: ${pathArg}`);
600
- }
601
-
602
- const url = `${this.baseUrl}/${pathArg}`;
603
- return await this.withRemoteResponse(url, {}, async (responseArg) => {
604
- if (!responseArg.ok) {
605
- throw new Error(`HTTP ${responseArg.status} for ${url}`);
606
- }
607
- return await responseArg.text();
608
- });
609
- }
610
-
611
- private async fetchTextFromUrl(urlArg: string): Promise<string> {
612
- return await this.withRemoteResponse(urlArg, {}, async (responseArg) => {
613
- if (!responseArg.ok) {
614
- throw new Error(`HTTP ${responseArg.status} for ${urlArg}`);
615
- }
616
- return await responseArg.text();
617
- });
618
- }
619
-
620
- private async fetchDockerManifestDigest(imageArg: IParsedDockerImageReference): Promise<string | null> {
621
- const manifestUrl = `https://${imageArg.registry}/v2/${imageArg.repository}/manifests/${imageArg.tag}`;
622
- const headers = new Headers({
623
- Accept: [
624
- 'application/vnd.docker.distribution.manifest.v2+json',
625
- 'application/vnd.oci.image.manifest.v1+json',
626
- 'application/vnd.docker.distribution.manifest.list.v2+json',
627
- 'application/vnd.oci.image.index.v1+json',
628
- ].join(', '),
629
- });
630
-
631
- let response = await this.fetchRemoteHeaders(manifestUrl, { method: 'HEAD', headers });
632
- if (response.status === 401) {
633
- const authHeader = response.headers.get('www-authenticate');
634
- const token = authHeader ? await this.fetchDockerRegistryToken(authHeader, imageArg.repository) : null;
635
- if (token) {
636
- headers.set('Authorization', `Bearer ${token}`);
637
- response = await this.fetchRemoteHeaders(manifestUrl, { method: 'HEAD', headers });
638
- }
639
- }
640
-
641
- if (!response.ok || !response.headers.get('docker-content-digest')) {
642
- response = await this.fetchRemoteHeaders(manifestUrl, { method: 'GET', headers });
643
- }
644
-
645
- if (!response.ok) {
646
- throw new Error(`HTTP ${response.status} while resolving ${imageArg.repository}:${imageArg.tag}`);
647
- }
648
-
649
- return response.headers.get('docker-content-digest');
650
- }
651
-
652
- private async fetchDockerRegistryToken(authHeaderArg: string, repositoryArg: string): Promise<string | null> {
653
- const match = authHeaderArg.match(/^Bearer\s+(.+)$/i);
654
- if (!match) return null;
655
-
656
- const authParams = new Map<string, string>();
657
- for (const partArg of match[1].match(/(?:[^,\"]+|\"[^\"]*\")+/g) || []) {
658
- const [key, rawValue] = partArg.split('=');
659
- if (!key || rawValue === undefined) continue;
660
- authParams.set(key.trim(), rawValue.trim().replace(/^\"|\"$/g, ''));
661
- }
662
-
663
- const realm = authParams.get('realm');
664
- if (!realm) return null;
665
- const tokenUrl = new URL(realm);
666
- const service = authParams.get('service');
667
- const scope = authParams.get('scope') || `repository:${repositoryArg}:pull`;
668
- if (service) tokenUrl.searchParams.set('service', service);
669
- tokenUrl.searchParams.set('scope', scope);
670
-
671
- const tokenResponse = await this.withRemoteResponse<{
672
- token?: string;
673
- access_token?: string;
674
- } | null>(tokenUrl.toString(), {}, async (responseArg) => {
675
- if (!responseArg.ok) return null;
676
- return await responseArg.json() as { token?: string; access_token?: string };
677
- });
678
- return tokenResponse?.token || tokenResponse?.access_token || null;
679
- }
680
-
681
- private validateVolume(volumeArg: interfaces.appstore.IAppStoreVolume, labelArg: string): void {
682
- const unknownKeys = Object.keys(volumeArg).filter((keyArg) => ![
683
- 'name',
684
- 'source',
685
- 'mountPath',
686
- 'driver',
687
- 'readOnly',
688
- 'backup',
689
- 'options',
690
- ].includes(keyArg));
691
- if (unknownKeys.length > 0) {
692
- throw new Error(`Invalid ${labelArg}: unknown field(s) ${unknownKeys.sort().join(', ')}`);
693
- }
694
- if (!volumeArg.mountPath
695
- || interfaces.data.normalizeServiceAbsolutePath(volumeArg.mountPath) !== volumeArg.mountPath
696
- || volumeArg.mountPath === '/') {
697
- throw new Error(`Invalid ${labelArg}: mountPath must be a canonical non-root absolute path`);
698
- }
699
- if (volumeArg.source !== undefined
700
- || volumeArg.driver !== undefined
701
- || volumeArg.options !== undefined) {
702
- throw new Error(`Invalid ${labelArg}: physical source, driver, and options fields are not portable`);
703
- }
704
- if (volumeArg.name !== undefined
705
- && !/^[a-z][a-z0-9-]{0,62}$/.test(volumeArg.name)) {
706
- throw new Error(`Invalid ${labelArg}: name must be a stable lowercase identifier`);
707
- }
708
- }
709
-
710
- private validatePublishedPorts(
711
- publishedPortsArg: TAppStoreVersionConfig['publishedPorts'] = [],
712
- labelArg: string,
713
- ): void {
714
- const seenPublishedPorts = new Set<string>();
715
- for (const portArg of publishedPortsArg) {
716
- const protocol = portArg.protocol || 'tcp';
717
- const targetStart = portArg.targetPort;
718
- const targetEnd = portArg.targetPortEnd || targetStart;
719
- const publishedStart = portArg.publishedPort || targetStart;
720
- const publishedEnd = portArg.publishedPortEnd || (publishedStart + (targetEnd - targetStart));
721
- const hostIp = portArg.hostIp || '0.0.0.0';
722
-
723
- if (!['tcp', 'udp'].includes(protocol)) {
724
- throw new Error(`Invalid ${labelArg}: published port protocol '${protocol}' is not supported`);
725
- }
726
- this.assertValidPort(targetStart, `${labelArg} targetPort`);
727
- this.assertValidPort(targetEnd, `${labelArg} targetPortEnd`);
728
- this.assertValidPort(publishedStart, `${labelArg} publishedPort`);
729
- this.assertValidPort(publishedEnd, `${labelArg} publishedPortEnd`);
730
- if (targetEnd < targetStart || publishedEnd < publishedStart) {
731
- throw new Error(`Invalid ${labelArg}: published port ranges must be ascending`);
732
- }
733
- if ((targetEnd - targetStart) !== (publishedEnd - publishedStart)) {
734
- throw new Error(`Invalid ${labelArg}: target and published port ranges must have the same size`);
735
- }
736
- if ((targetEnd - targetStart) > 1000) {
737
- throw new Error(`Invalid ${labelArg}: published port ranges may not exceed 1001 ports`);
738
- }
739
-
740
- for (let offset = 0; offset <= targetEnd - targetStart; offset++) {
741
- const publishedPort = publishedStart + offset;
742
- const publishedKey = `${hostIp}/${protocol}/${publishedPort}`;
743
- const wildcardKey = `0.0.0.0/${protocol}/${publishedPort}`;
744
- const conflictsWithWildcard = hostIp === '0.0.0.0'
745
- ? Array.from(seenPublishedPorts).some((keyArg) => keyArg.endsWith(`/${protocol}/${publishedPort}`))
746
- : seenPublishedPorts.has(wildcardKey);
747
- if (seenPublishedPorts.has(publishedKey) || conflictsWithWildcard) {
748
- throw new Error(`Invalid ${labelArg}: duplicate published port ${hostIp}:${publishedPort}/${protocol}`);
749
- }
750
- seenPublishedPorts.add(publishedKey);
751
- }
752
- }
753
- }
754
-
755
- private validateTargetPortModel(
756
- configArg: TAppStoreVersionConfig,
757
- labelArg: string,
758
- ): void {
759
- if (configArg.port === undefined
760
- && (!configArg.targetPorts || configArg.targetPorts.length === 0)) {
761
- throw new Error(`Invalid ${labelArg}: either port or targetPorts is required`);
762
- }
763
- try {
764
- interfaces.data.normalizeServicePortConfig({
765
- name: labelArg,
766
- ports: configArg.port !== undefined ? { web: configArg.port } : undefined,
767
- targetPorts: configArg.targetPorts,
768
- domains: configArg.domains,
769
- publicPortMappings: configArg.publicPortMappings,
770
- });
771
- } catch (error) {
772
- throw new Error(`Invalid ${labelArg}: ${(error as Error).message}`);
773
- }
774
- }
775
-
776
- private assertValidPort(portArg: number, labelArg: string): void {
777
- if (!Number.isInteger(portArg) || portArg < 1 || portArg > 65535) {
778
- throw new Error(`Invalid ${labelArg}: ${portArg}. Expected an integer port between 1 and 65535.`);
779
- }
780
- }
781
- }
782
-
783
- export function parseAppStoreIndex(inputArg: unknown): TAppStoreIndex {
784
- const appStore = inputArg as TAppStoreIndex;
785
- if (!appStore || typeof appStore !== 'object' || !Array.isArray(appStore.apps)) {
786
- throw new Error('Invalid appstore format');
787
- }
788
- return appStore;
789
- }
790
-
791
- export function parseServezoneAppStoreManifest(inputArg: unknown): TServezoneAppStoreManifest {
792
- const manifest = inputArg as TServezoneAppStoreManifest;
793
- if (!manifest || typeof manifest !== 'object') {
794
- throw new Error('Appstore manifest must be an object');
795
- }
796
- if (!manifest.app?.id || !manifest.app?.name) {
797
- throw new Error('Appstore manifest app.id and app.name are required');
798
- }
799
- return manifest;
800
- }
801
-
802
- export function parseDockerImageReference(imageArg: string): IParsedDockerImageReference {
803
- const [imageWithoutDigest, digest] = imageArg.split('@');
804
- const imageParts = imageWithoutDigest.split('/');
805
- const firstPart = imageParts[0];
806
- const hasExplicitRegistry = imageParts.length > 1
807
- && (firstPart.includes('.') || firstPart.includes(':') || firstPart === 'localhost');
808
- const registry = hasExplicitRegistry ? firstPart : 'registry-1.docker.io';
809
- const repositoryParts = hasExplicitRegistry ? imageParts.slice(1) : imageParts;
810
- let repositoryWithTag = repositoryParts.join('/');
811
- if (!repositoryWithTag) {
812
- throw new Error(`Invalid Docker image reference '${imageArg}'`);
813
- }
814
-
815
- if (!hasExplicitRegistry && !repositoryWithTag.includes('/')) {
816
- repositoryWithTag = `library/${repositoryWithTag}`;
817
- }
818
-
819
- const lastSlashIndex = repositoryWithTag.lastIndexOf('/');
820
- const lastColonIndex = repositoryWithTag.lastIndexOf(':');
821
- const hasTag = lastColonIndex > lastSlashIndex;
822
- const repository = hasTag ? repositoryWithTag.slice(0, lastColonIndex) : repositoryWithTag;
823
- const tag = hasTag ? repositoryWithTag.slice(lastColonIndex + 1) : 'latest';
824
-
825
- return { registry, repository, tag, digest };
826
- }
827
-
828
- export function createAppStoreVersionForDockerSource(
829
- sourceArg: TAppStoreDockerImageSource,
830
- fallbackVersionArg: string,
831
- digestArg?: string,
832
- ): string {
833
- if (sourceArg.tracking !== 'digest' || !digestArg) {
834
- return fallbackVersionArg;
835
- }
836
- const parsedImage = parseDockerImageReference(sourceArg.image);
837
- return `${parsedImage.tag}@${digestArg}`;
838
- }
839
-
840
- async function createSha256Hex(inputArg: string): Promise<string> {
841
- const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(inputArg));
842
- return Array.from(new Uint8Array(digest))
843
- .map((byteArg) => byteArg.toString(16).padStart(2, '0'))
844
- .join('');
845
- }
846
-
847
- function selectHighestSemanticVersion(versionsArg: string[]): string | null {
848
- if (versionsArg.length === 0) return null;
849
- const parsedVersions: SmartVersion[] = [];
850
- for (const versionArg of versionsArg) {
851
- try {
852
- parsedVersions.push(new SmartVersion(versionArg));
853
- } catch {
854
- return null;
855
- }
856
- }
857
- let highestIndex = 0;
858
- for (let index = 1; index < parsedVersions.length; index++) {
859
- if (parsedVersions[index].greaterThan(parsedVersions[highestIndex])) {
860
- highestIndex = index;
861
- }
862
- }
863
- return versionsArg[highestIndex];
864
- }
865
-
866
- function selectImplicitLatestVersion(versionsArg: string[], labelArg: string): string {
867
- if (versionsArg.length === 0) {
868
- throw new Error(`${labelArg} defines no versions`);
869
- }
870
- if (versionsArg.length === 1) return versionsArg[0];
871
- const semverLatest = selectHighestSemanticVersion(versionsArg);
872
- if (!semverLatest) {
873
- throw new Error(
874
- `${labelArg} must declare latestVersion or a channel when multiple non-semver versions exist`,
875
- );
876
- }
877
- return semverLatest;
878
- }
879
-
880
- function uniqueStrings(valuesArg: string[]): string[] {
881
- return Array.from(new Set(valuesArg.filter(Boolean)));
882
- }
883
-
884
- function withoutUndefined<T extends object>(objectArg: T): Partial<T> {
885
- return Object.fromEntries(
886
- Object.entries(objectArg).filter(([, valueArg]) => valueArg !== undefined),
887
- ) as Partial<T>;
888
- }
889
-
890
- function getUpgradeStrategyForConfig(
891
- configArg?: TAppStoreVersionConfig,
892
- ): interfaces.appstore.IAppStoreApp['upgradeStrategy'] {
893
- if (configArg?.upgradeStrategy) return configArg.upgradeStrategy;
894
- if (configArg?.source?.type === 'dockerImage' && configArg.source.tracking === 'digest') return 'dockerDigest';
895
- return undefined;
896
- }