@serve.zone/appstore 32.27.0 → 33.0.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 +57 -0
  3. package/dist_catalog/catalog.digest +1 -0
  4. package/dist_catalog/catalog.envelope.json +7440 -0
  5. package/{dist_ts_client/data.generated.js → dist_catalog/catalog.json} +6740 -6913
  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 +15 -10
  27. package/readme.md +161 -92
  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,18 +1,7 @@
1
- import type * as interfaces from '@serve.zone/interfaces';
2
-
3
- export type TAppStoreFetch = typeof fetch;
4
- export type TAppStoreSourceMode = 'package' | 'remote';
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, TAppStoreStorageClass>;
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
- export interface IAppStoreResolverOptions {
52
- source?: TAppStoreSourceMode;
53
- baseUrl?: string;
54
- fetch?: TAppStoreFetch;
55
- resolveDockerDigests?: boolean;
56
- requestTimeoutMs?: number;
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
- export interface IAppStorePackageData {
61
- generatedAt: string;
62
- jsonFiles: Record<string, unknown>;
63
- textFiles: Record<string, string>;
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
- export interface IAppStoreSourceInfo {
67
- source: TAppStoreSourceMode;
68
- packageName: string;
69
- packageVersion: string;
70
- baseUrl?: string;
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
- export interface IResolvedAppStoreApp {
74
- appStoreApp: TAppStoreApp;
75
- appMeta: TAppStoreAppMeta;
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
- export interface IParsedDockerImageReference {
80
- registry: string;
81
- repository: string;
82
- tag: string;
83
- digest?: string;
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 };
@@ -0,0 +1,3 @@
1
+ {
2
+ "order": 2
3
+ }
@@ -1,10 +0,0 @@
1
- {
2
- "id": "adminer",
3
- "name": "Adminer",
4
- "description": "Lightweight database management tool with web interface",
5
- "category": "Databases",
6
- "iconName": "adminer",
7
- "latestVersion": "1.0.0",
8
- "versions": ["1.0.0"],
9
- "maintainer": "serve.zone"
10
- }
@@ -1,6 +0,0 @@
1
- {
2
- "image": "adminer:latest",
3
- "targetPorts": [
4
- { "name": "web", "port": 8080, "protocol": "http", "default": true }
5
- ]
6
- }
@@ -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
- }