@serve.zone/appstore 1.3.0 → 1.4.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/changelog.md +12 -0
- package/dist_ts_client/classes.appstoreresolver.d.ts +7 -1
- package/dist_ts_client/classes.appstoreresolver.js +37 -14
- package/dist_ts_client/index.d.ts +1 -0
- package/dist_ts_client/index.js +2 -1
- package/dist_ts_client/storage.d.ts +13 -0
- package/dist_ts_client/storage.js +635 -0
- package/dist_ts_client/types.d.ts +31 -0
- package/package.json +2 -2
- package/readme.md +141 -1
- package/ts_client/classes.appstoreresolver.ts +46 -16
- package/ts_client/index.ts +1 -0
- package/ts_client/storage.ts +831 -0
- package/ts_client/types.ts +38 -0
|
@@ -9,6 +9,37 @@ export type TAppStoreDockerImageSource = interfaces.appstore.IAppStoreDockerImag
|
|
|
9
9
|
export type TAppStoreRepoManifestSource = interfaces.appstore.IAppStoreRepoManifestSource;
|
|
10
10
|
export type TAppStoreResolvedSource = interfaces.appstore.IAppStoreResolvedSource;
|
|
11
11
|
export type TServezoneAppStoreManifest = interfaces.appstore.IServezoneAppStoreManifest;
|
|
12
|
+
export type TAppStoreStorageRequest = interfaces.appstore.TAppStoreStorageRequest;
|
|
13
|
+
export type TAppStoreStorageClass = interfaces.appstore.IAppStoreStorageClass;
|
|
14
|
+
export type TAppStoreStorageFeatureId = interfaces.appstore.TAppStoreStorageFeatureId;
|
|
15
|
+
export type TAppStoreStorageRequestOrigin = 'explicit' | 'legacyVolume' | 'legacyS3';
|
|
16
|
+
export interface IAppStoreStorageLegacyCompatibility {
|
|
17
|
+
/**
|
|
18
|
+
* Downstream adapters preserve the existing S3/AWS environment aliases for
|
|
19
|
+
* this request. New named bindings use their explicit delivery declaration.
|
|
20
|
+
*/
|
|
21
|
+
objectStorage?: {
|
|
22
|
+
requestId: 'legacy-s3';
|
|
23
|
+
deliveryProfile: 'servezone-platform-s3-v1';
|
|
24
|
+
/** Legacy binding values replace same-key template env values. */
|
|
25
|
+
environmentOwnership: 'bindingOverridesTemplate';
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface INormalizedAppStoreStorageConfig {
|
|
29
|
+
schemaVersion: 1;
|
|
30
|
+
storageClasses: Record<string, TAppStoreStorageClass>;
|
|
31
|
+
storageRequests: TAppStoreStorageRequest[];
|
|
32
|
+
requestOrigins: Record<string, TAppStoreStorageRequestOrigin>;
|
|
33
|
+
requiredFeatureIds: TAppStoreStorageFeatureId[];
|
|
34
|
+
legacyCompatibility?: IAppStoreStorageLegacyCompatibility;
|
|
35
|
+
}
|
|
36
|
+
export interface IAppStoreStorageNormalizationOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Portable storage features advertised by the fulfillment adapter.
|
|
39
|
+
* Explicit storage requests fail closed when any required feature is absent.
|
|
40
|
+
*/
|
|
41
|
+
supportedFeatures: Iterable<string>;
|
|
42
|
+
}
|
|
12
43
|
export interface IAppStoreResolverOptions {
|
|
13
44
|
source?: TAppStoreSourceMode;
|
|
14
45
|
baseUrl?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/appstore",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "serve.zone App Store metadata, parser, and resolver client.",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"author": "Task Venture Capital GmbH",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@serve.zone/interfaces": "
|
|
13
|
+
"@serve.zone/interfaces": "18.1.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@git.zone/tsbuild": "^4.4.2",
|
package/readme.md
CHANGED
|
@@ -57,7 +57,7 @@ Some templates ask Onebox to provision local platform services and inject connec
|
|
|
57
57
|
|
|
58
58
|
| App ID | Requirements | Notes |
|
|
59
59
|
| --- | --- | --- |
|
|
60
|
-
| `cloudly` | MongoDB, S3 | Requires Onebox `2.2.0` or newer. Uses provisioned MongoDB and
|
|
60
|
+
| `cloudly` | MongoDB, S3 | Requires Onebox `2.2.0` or newer. Uses provisioned MongoDB and managed S3-compatible storage for Cloudly state and registry storage, and reports initial admin credentials through hosted-app lifecycle. |
|
|
61
61
|
| `ghost` | MariaDB | Uses MySQL-style `database__connection__*` environment variables. |
|
|
62
62
|
| `gitea` | MariaDB | Uses Gitea database environment variables with `${MARIADB_*}` placeholders. |
|
|
63
63
|
| `nextcloud` | MariaDB, Valkey | Uses MySQL-compatible database variables. Valkey is declared as a platform requirement. |
|
|
@@ -94,6 +94,144 @@ Version configs use a small, pragmatic schema understood by serve.zone runtimes:
|
|
|
94
94
|
}
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
`platformRequirements.s3: true` and `volumes` are legacy inputs. The resolver
|
|
98
|
+
normalizes them deterministically for existing templates, but new templates use
|
|
99
|
+
portable, template-local `storageClasses` and stable named `storageRequests`.
|
|
100
|
+
Legacy `platformRequirements.s3: true` cannot coexist with an explicit
|
|
101
|
+
`objectStorage` request. Legacy volume objects may retain `name`, `mountPath`,
|
|
102
|
+
`readOnly`, and `backup`; `source`, `driver`, and `options` fail validation
|
|
103
|
+
because they cannot be normalized portably.
|
|
104
|
+
|
|
105
|
+
## Portable Storage Requests
|
|
106
|
+
|
|
107
|
+
Filesystem mounts and managed object-storage resources are different request
|
|
108
|
+
kinds in the same portable contract. An app can request both, including more
|
|
109
|
+
than one isolated object-storage binding:
|
|
110
|
+
|
|
111
|
+
```jsonc
|
|
112
|
+
{
|
|
113
|
+
"storageClasses": {
|
|
114
|
+
"databaseFast": {
|
|
115
|
+
"kind": "filesystem",
|
|
116
|
+
"purpose": "database",
|
|
117
|
+
"required": {
|
|
118
|
+
"performanceTier": "highIops",
|
|
119
|
+
"durability": "persistent",
|
|
120
|
+
"hardQuota": true,
|
|
121
|
+
"snapshots": "native",
|
|
122
|
+
"backup": true,
|
|
123
|
+
"encryptedInTransit": true
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"backupCapacity": {
|
|
127
|
+
"kind": "objectStorage",
|
|
128
|
+
"purpose": "backup",
|
|
129
|
+
"required": {
|
|
130
|
+
"performanceTier": "capacity",
|
|
131
|
+
"durability": "persistent",
|
|
132
|
+
"hardQuota": true,
|
|
133
|
+
"backup": true,
|
|
134
|
+
"encryptedInTransit": true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"storageRequests": [
|
|
139
|
+
{
|
|
140
|
+
"id": "database-data",
|
|
141
|
+
"kind": "filesystem",
|
|
142
|
+
"storageClass": "databaseFast",
|
|
143
|
+
"mountPath": "/var/lib/example",
|
|
144
|
+
"accessMode": "ReadWriteOnce",
|
|
145
|
+
"capacity": { "request": "20GiB", "limit": "40GiB" },
|
|
146
|
+
"reclaimPolicy": "retain",
|
|
147
|
+
"protection": {
|
|
148
|
+
"backup": "required",
|
|
149
|
+
"snapshots": "native",
|
|
150
|
+
"consistency": "crashConsistent"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "backup-archive",
|
|
155
|
+
"kind": "objectStorage",
|
|
156
|
+
"storageClass": "backupCapacity",
|
|
157
|
+
"accessMode": "readWrite",
|
|
158
|
+
"capacity": { "request": "100GiB", "limit": "1TiB" },
|
|
159
|
+
"reclaimPolicy": "retain",
|
|
160
|
+
"delivery": {
|
|
161
|
+
"type": "secretFile",
|
|
162
|
+
"targetPath": "/run/secrets/backup-archive.json",
|
|
163
|
+
"format": "servezone-object-storage-v1"
|
|
164
|
+
},
|
|
165
|
+
"protection": {
|
|
166
|
+
"backup": "required",
|
|
167
|
+
"versioning": "required",
|
|
168
|
+
"retentionDays": 30
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "registry-capacity",
|
|
173
|
+
"kind": "objectStorage",
|
|
174
|
+
"storageClass": "backupCapacity",
|
|
175
|
+
"accessMode": "readWrite",
|
|
176
|
+
"reclaimPolicy": "retain",
|
|
177
|
+
"delivery": {
|
|
178
|
+
"type": "environment",
|
|
179
|
+
"keys": {
|
|
180
|
+
"endpoint": "REGISTRY_S3_ENDPOINT",
|
|
181
|
+
"bucket": "REGISTRY_S3_BUCKET",
|
|
182
|
+
"region": "REGISTRY_S3_REGION",
|
|
183
|
+
"accessKeyId": "REGISTRY_S3_ACCESS_KEY_ID",
|
|
184
|
+
"secretAccessKey": "REGISTRY_S3_SECRET_ACCESS_KEY"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"requiresFeatures": [
|
|
190
|
+
"storage.bindings.v1",
|
|
191
|
+
"storage.filesystem.v1",
|
|
192
|
+
"storage.object-storage.v1",
|
|
193
|
+
"storage.object-storage.secret-file.v1"
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Capacity values are positive integers followed by `KiB`, `MiB`, `GiB`, or
|
|
199
|
+
`TiB`. A `limit` requires `storageClass.required.hardQuota: true`. For explicit
|
|
200
|
+
requests, delivery environment keys and secret-file paths must be unique across
|
|
201
|
+
the app config; provider credential values never belong in an App Store manifest.
|
|
202
|
+
|
|
203
|
+
The full version config and every storage object are exact-key validated.
|
|
204
|
+
Classes express portable policy only. Provider addresses, protocols,
|
|
205
|
+
identities, host paths, drivers, and driver options are rejected instead of
|
|
206
|
+
being passed through. Unknown `storage.*` feature IDs also fail closed. Request
|
|
207
|
+
IDs are stable across upgrades, restores, and migrations.
|
|
208
|
+
|
|
209
|
+
Before provisioning, a runtime must advertise its supported feature IDs and
|
|
210
|
+
call the resolver's fail-closed normalization entry point:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const normalizedStorage = resolver.normalizeStorageConfig(config, {
|
|
214
|
+
supportedFeatures: capabilityAdvertisement.featureIds,
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The package also exports the pure `validateAppStoreStorageConfig()` and
|
|
219
|
+
`normalizeAppStoreStorageConfig()` functions, `parseStorageCapacityQuantity()`,
|
|
220
|
+
and the normalized storage handoff types. Structural validation is useful for
|
|
221
|
+
catalog ingestion; only normalization with a target's `supportedFeatures` is a
|
|
222
|
+
valid pre-fulfillment gate.
|
|
223
|
+
|
|
224
|
+
The result contains canonical `storageClasses`, `storageRequests`,
|
|
225
|
+
`requestOrigins`, and `requiredFeatureIds`. `requestOrigins` marks explicit,
|
|
226
|
+
legacy-volume, and legacy-S3 requests so fulfillment adapters can preserve old
|
|
227
|
+
delivery behavior without weakening the new contract. Legacy S3 normalization
|
|
228
|
+
uses the versioned `servezone-platform-s3-v1` delivery profile; adapters keep
|
|
229
|
+
the established S3/AWS environment aliases for that profile. Those
|
|
230
|
+
binding-provided values deterministically replace same-key legacy template
|
|
231
|
+
environment values. Legacy inputs do not claim new feature support, while every
|
|
232
|
+
explicit request fails before provisioning if a derived storage feature is
|
|
233
|
+
unavailable.
|
|
234
|
+
|
|
97
235
|
Environment variable keys must start with a letter or underscore and may contain letters, numbers, and underscores. Mixed-case keys are supported because some upstream images, including Ghost and Gitea, require lowercase nested configuration segments such as `database__connection__host` or `GITEA__database__HOST`.
|
|
98
236
|
|
|
99
237
|
Linked app manifests use the same runtime schema and let the upstream app repo own its install and upgrade metadata:
|
|
@@ -150,6 +288,8 @@ Only include keys that are actually needed by a template. For example, `nginx` i
|
|
|
150
288
|
- Prefer package-published App Store data for runtime consumers. Use `source.type=repoManifest` only when the linked manifest is intentionally reachable from the runtime environment, or pre-resolve it before publishing an App Store package.
|
|
151
289
|
- Prefer explicit environment variable descriptions because Onebox surfaces them to users during installation.
|
|
152
290
|
- Use platform placeholders such as `${MARIADB_HOST}`, `${MARIADB_PORT}`, `${MARIADB_DATABASE}`, `${MARIADB_USER}`, `${MARIADB_PASSWORD}`, `${MONGODB_URI}`, `${S3_BUCKET}`, `${S3_ACCESS_KEY}`, and `${S3_SECRET_KEY}` only when the matching platform service is declared.
|
|
291
|
+
- Use named `objectStorage` requests instead of `platformRequirements.s3` in new templates, with a distinct delivery destination for every binding.
|
|
292
|
+
- Use named `filesystem` requests instead of `volumes` in new templates. Never put provider, host-mount, protocol, identity, driver, or driver-option details in portable manifests.
|
|
153
293
|
- Keep template images boring and operationally safe unless there is a reason to pin a more specific upstream tag.
|
|
154
294
|
- Validate non-trivial templates by installing them through Onebox instead of only checking JSON syntax.
|
|
155
295
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import * as interfaces from '@serve.zone/interfaces';
|
|
2
2
|
import packageJson from '../package.json' with { type: 'json' };
|
|
3
3
|
import { appStorePackageData } from './data.generated.js';
|
|
4
|
+
import {
|
|
5
|
+
normalizeAppStoreStorageConfig,
|
|
6
|
+
validateAppStoreStorageConfig,
|
|
7
|
+
} from './storage.js';
|
|
4
8
|
import type {
|
|
5
9
|
IAppStoreResolverOptions,
|
|
6
10
|
IAppStoreSourceInfo,
|
|
11
|
+
IAppStoreStorageNormalizationOptions,
|
|
12
|
+
INormalizedAppStoreStorageConfig,
|
|
7
13
|
IParsedDockerImageReference,
|
|
8
14
|
IResolvedAppStoreApp,
|
|
9
15
|
TAppStoreApp,
|
|
@@ -252,14 +258,7 @@ export class AppStoreResolver {
|
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
|
|
256
|
-
const secretFileErrors = interfaces.data.validateServiceSecretFiles(
|
|
257
|
-
configArg.secretFiles,
|
|
258
|
-
normalizedVolumes.map((volumeArg) => volumeArg.mountPath),
|
|
259
|
-
);
|
|
260
|
-
if (secretFileErrors.length > 0) {
|
|
261
|
-
throw new Error(`Invalid ${labelArg}: ${secretFileErrors.join('; ')}`);
|
|
262
|
-
}
|
|
261
|
+
validateAppStoreStorageConfig(configArg, labelArg);
|
|
263
262
|
for (const secretFile of configArg.secretFiles || []) {
|
|
264
263
|
if (!secretEnvVarKeys.has(secretFile.sourceKey)) {
|
|
265
264
|
throw new Error(
|
|
@@ -274,6 +273,20 @@ export class AppStoreResolver {
|
|
|
274
273
|
this.validatePublishedPorts(configArg.publishedPorts || [], labelArg);
|
|
275
274
|
}
|
|
276
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Produces the canonical storage handoff only after target feature
|
|
278
|
+
* negotiation. Fulfillment adapters should use this method before creating
|
|
279
|
+
* or mutating any storage resource.
|
|
280
|
+
*/
|
|
281
|
+
public normalizeStorageConfig(
|
|
282
|
+
configArg: TAppStoreVersionConfig,
|
|
283
|
+
optionsArg: IAppStoreStorageNormalizationOptions,
|
|
284
|
+
labelArg = 'appstore config',
|
|
285
|
+
): INormalizedAppStoreStorageConfig {
|
|
286
|
+
this.validateAppStoreVersionConfig(configArg, labelArg);
|
|
287
|
+
return normalizeAppStoreStorageConfig(configArg, optionsArg, labelArg);
|
|
288
|
+
}
|
|
289
|
+
|
|
277
290
|
public normalizeVolumes(
|
|
278
291
|
volumesArg: TAppStoreVersionConfig['volumes'] = [],
|
|
279
292
|
): interfaces.appstore.IAppStoreVolume[] {
|
|
@@ -569,14 +582,31 @@ export class AppStoreResolver {
|
|
|
569
582
|
}
|
|
570
583
|
|
|
571
584
|
private validateVolume(volumeArg: interfaces.appstore.IAppStoreVolume, labelArg: string): void {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
585
|
+
const unknownKeys = Object.keys(volumeArg).filter((keyArg) => ![
|
|
586
|
+
'name',
|
|
587
|
+
'source',
|
|
588
|
+
'mountPath',
|
|
589
|
+
'driver',
|
|
590
|
+
'readOnly',
|
|
591
|
+
'backup',
|
|
592
|
+
'options',
|
|
593
|
+
].includes(keyArg));
|
|
594
|
+
if (unknownKeys.length > 0) {
|
|
595
|
+
throw new Error(`Invalid ${labelArg}: unknown field(s) ${unknownKeys.sort().join(', ')}`);
|
|
596
|
+
}
|
|
597
|
+
if (!volumeArg.mountPath
|
|
598
|
+
|| interfaces.data.normalizeServiceAbsolutePath(volumeArg.mountPath) !== volumeArg.mountPath
|
|
599
|
+
|| volumeArg.mountPath === '/') {
|
|
600
|
+
throw new Error(`Invalid ${labelArg}: mountPath must be a canonical non-root absolute path`);
|
|
601
|
+
}
|
|
602
|
+
if (volumeArg.source !== undefined
|
|
603
|
+
|| volumeArg.driver !== undefined
|
|
604
|
+
|| volumeArg.options !== undefined) {
|
|
605
|
+
throw new Error(`Invalid ${labelArg}: physical source, driver, and options fields are not portable`);
|
|
606
|
+
}
|
|
607
|
+
if (volumeArg.name !== undefined
|
|
608
|
+
&& !/^[a-z][a-z0-9-]{0,62}$/.test(volumeArg.name)) {
|
|
609
|
+
throw new Error(`Invalid ${labelArg}: name must be a stable lowercase identifier`);
|
|
580
610
|
}
|
|
581
611
|
}
|
|
582
612
|
|
package/ts_client/index.ts
CHANGED