@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.
- package/.smartconfig.json +4 -1
- package/changelog.md +57 -0
- package/dist_catalog/catalog.digest +1 -0
- package/dist_catalog/catalog.envelope.json +7440 -0
- package/{dist_ts_client/data.generated.js → dist_catalog/catalog.json} +6740 -6913
- 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 +15 -10
- package/readme.md +161 -92
- 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/readme.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @serve.zone/appstore
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The serve.zone App Store: the curated app templates, the publisher that turns them into one signed runtime catalog, and the client Onebox and Cloudly use to load that catalog at runtime.
|
|
4
|
+
|
|
5
|
+
A consumer never bundles App Store data. It loads the signed catalog from an npm registry in the background, verifies it against the signing keys its own release trusts, keeps the last verified catalog as its last-known-good copy, and picks up new apps and versions without a release of its own. The App Store is never a startup blocker: while no verified catalog is active, only App Store install, upgrade and upgrade-preview are refused.
|
|
4
6
|
|
|
5
7
|
## Issue Reporting and Security
|
|
6
8
|
|
|
@@ -18,41 +20,43 @@ The client speaks the `@serve.zone/interfaces` 32 App Store contracts. The stora
|
|
|
18
20
|
|
|
19
21
|
| File or directory | Purpose |
|
|
20
22
|
| --- | --- |
|
|
21
|
-
| `
|
|
22
|
-
| `apps/<app>/
|
|
23
|
-
| `apps/<app>/versions/<version>/
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
23
|
+
| `apps/<app>/app.json` | The app's catalog `info` (`id`, `name`, `description`, `category`, optional `iconName`, `iconUrl`, `tags`, `maintainer`, `links`), its `latestVersion` and the `versions` it publishes, in order. |
|
|
24
|
+
| `apps/<app>/versions/<version>/config.json` | The version config: digest-pinned image, ports, environment, storage, platform requirements, compatibility minimums and upgrade flags. |
|
|
25
|
+
| `apps/<app>/versions/<version>/withdrawn.json` | Present once a version is withdrawn: `{ "at", "reason" }`. A withdrawal is final. |
|
|
26
|
+
| `catalog.lock.json` | The publication record: every published `app@version` with its config digest and withdrawal. It only grows. |
|
|
27
|
+
| `ts_publisher/` | The catalog publisher (not published): builds `dist_catalog/`, signs it, maintains the lock. |
|
|
28
|
+
| `ts_client/` | The published `@serve.zone/appstore` client: the catalog loader, catalog lookups and the storage handoff. |
|
|
29
|
+
| `dist_catalog/` | Build output, published with the package: `catalog.json`, `catalog.digest` and, in a release, the signed `catalog.envelope.json`. |
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
Nothing else may sit in an app or version folder; the publisher refuses any other file.
|
|
28
32
|
|
|
29
33
|
## Current App Store
|
|
30
34
|
|
|
31
|
-
The
|
|
32
|
-
|
|
33
|
-
| App ID | Name | App Store category | Image | Port |
|
|
34
|
-
| --- | --- | --- | --- | --- |
|
|
35
|
-
| `adminer` | Adminer | Dev Tools | `adminer:
|
|
36
|
-
| `cloudly` | Cloudly | Dev Tools | `code.foss.global/serve.zone/cloudly@sha256:700bb723fef408a6bf8079f643f9990247bffe6541403fd421d064ba8128a447` | `3000` |
|
|
37
|
-
| `ghost` | Ghost | CMS | `ghost:
|
|
38
|
-
| `gitea` | Gitea | Dev Tools | `gitea/gitea:
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
The catalog lists 19 apps. Every image is pinned to the digest of its multi-platform index.
|
|
36
|
+
|
|
37
|
+
| App ID | Name | App Store category | Latest | Image | Port |
|
|
38
|
+
| --- | --- | --- | --- | --- | --- |
|
|
39
|
+
| `adminer` | Adminer | Dev Tools | `1.0.0` | `adminer@sha256:74f29c416e148b98305e84446a18db7cd2c1038264dec464359d36774ef080ce` | `8080` |
|
|
40
|
+
| `cloudly` | Cloudly | Dev Tools | `1.27.0` | `code.foss.global/serve.zone/cloudly@sha256:700bb723fef408a6bf8079f643f9990247bffe6541403fd421d064ba8128a447` | `3000` |
|
|
41
|
+
| `ghost` | Ghost | CMS | `1.0.0` | `ghost@sha256:9482099b1c8700764dc3d38c293e09c16d70f8bacc14af589c47d90b4bebd017` | `2368` |
|
|
42
|
+
| `gitea` | Gitea | Dev Tools | `1.0.0` | `gitea/gitea@sha256:a9dc25679ebdd33356242d2e0667cc220043745e8339922879f187c20329a42e` | `3000` |
|
|
43
|
+
| `grafana` | Grafana | Monitoring | `1.0.0` | `grafana/grafana@sha256:b28bae15e219c998fb0e0424ed724930cc61b1f61fb404d47c862f9a23f9e572` | `3000` |
|
|
44
|
+
| `mariadb` | MariaDB | Database | `1.0.0` | `mariadb@sha256:f1bba652ba57bea3099ca2fe1af692af537c27d96e0bcde39dce29e2ba1ec4f3` | `3306` |
|
|
45
|
+
| `mattermost` | Mattermost | Communication | `1.0.0` | `mattermost/mattermost-team-edition@sha256:6ad5912b45875ef2fafc41886b5a70a3ec1b6c17ec706e0492566e8a2fd8aa85` | `8065` |
|
|
46
|
+
| `n8n` | N8N | Automation | `1.0.0` | `n8nio/n8n@sha256:240eaa2a3d491adac5817aa4c3f1c521bb18ec79f6e448517158d5220ee0f37b` | `5678` |
|
|
47
|
+
| `nextcloud` | Nextcloud | Storage | `1.0.0` | `nextcloud@sha256:4ba2af4695e1801b5f66ed4df863c0fa0f236f632ffd04b2fdc32ee0c2fefe42` | `80` |
|
|
48
|
+
| `nginx` | Nginx | Web Server | `1.0.0` | `nginx@sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2` | `80` |
|
|
49
|
+
| `openbao` | OpenBao | Security | `1.0.0` | `openbao/openbao@sha256:900bb64d0671cd1d82b693c56206f7263b582445f3a3bb6ba6e5213f524a6653` | `8200` |
|
|
50
|
+
| `plausible` | Plausible Analytics | Analytics | `1.0.0` | `plausible/analytics@sha256:cd5f75e1399073669b13b4151cc603332a825324d0b8f13dfc9de9112a3c68a1` | `8000` |
|
|
51
|
+
| `portainer` | Portainer | Dev Tools | `1.0.0` | `portainer/portainer-ce@sha256:4d616db18cfeb5dd41a69c0958bc825c84483ea9cde1106eb82a5d26f3bd8b0e` | `9000` |
|
|
52
|
+
| `postgres` | PostgreSQL | Database | `1.0.0` | `postgres@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea` | `5432` |
|
|
53
|
+
| `rustdesk-server` | RustDesk Server | Remote Access | `1.0.0` | `rustdesk/rustdesk-server-s6@sha256:5974cd791a5aca6fbf01a167b37e7427db1d136e95b2710c33a34fdc9fea724c` | `21116` |
|
|
54
|
+
| `uptime-kuma` | Uptime Kuma | Monitoring | `1.0.0` | `louislam/uptime-kuma@sha256:70233f4acb5163fd2a59a49909cf01e44415cecff13dba69e3a86647a2919f83` | `3001` |
|
|
55
|
+
| `valkey` | Valkey | Database | `1.0.0` | `valkey/valkey@sha256:c9b77919daeba2c02ad954d0c844cc4e7142069d177b89c5fd771f405daf9e02` | `6379` |
|
|
56
|
+
| `vaultwarden` | Vaultwarden | Security | `1.0.0` | `vaultwarden/server@sha256:efb3cde962015fcc036b2ea625242248611943b27212ebf4392841fb30fad055` | `80` |
|
|
57
|
+
| `wordpress` | WordPress | CMS | `1.0.0` | `wordpress@sha256:736761c95c327cb21602ab9175ee4dba6e779b00f3f4895536a3e1af5ac4fe4c` | `80` |
|
|
58
|
+
|
|
59
|
+
Cloudly `1.0.0` and `1.1.0` are withdrawn: they were floating-tag templates (`cloudly:latest`) and stay listed, pinned to the image that tag served when the catalog was created, only so that services installed from them keep reading their config. Install `1.21.0` or later.
|
|
56
60
|
|
|
57
61
|
## Platform Requirements
|
|
58
62
|
|
|
@@ -69,7 +73,6 @@ Some templates ask Onebox to provision local platform services and inject connec
|
|
|
69
73
|
| `gitea` | MariaDB | Uses Gitea database environment variables with `${MARIADB_*}` placeholders. |
|
|
70
74
|
| `nextcloud` | MariaDB, Valkey | Uses MySQL-compatible database variables. Valkey is declared as a platform requirement. |
|
|
71
75
|
| `plausible` | ClickHouse | Declares ClickHouse as a platform requirement. |
|
|
72
|
-
| `siprouter` | MongoDB, S3 | Uses SmartData state and object storage for telephony-related runtime data. |
|
|
73
76
|
| `wordpress` | MariaDB | Uses standard WordPress database environment variables. |
|
|
74
77
|
|
|
75
78
|
Standalone templates such as `nginx`, `adminer`, `valkey`, `portainer`, `mattermost`, `n8n`, `uptime-kuma`, and `vaultwarden` currently define only image and port unless their version config adds explicit environment variables. `rustdesk-server` uses the official all-in-one s6 image and requires `RELAY` to advertise the public relay endpoint; RustDesk clients also require firewall access to TCP `21115`, TCP/UDP `21116`, TCP `21117`, and optional web-client TCP `21118`/`21119`.
|
|
@@ -80,7 +83,7 @@ Version configs use a small, pragmatic schema understood by serve.zone runtimes:
|
|
|
80
83
|
|
|
81
84
|
```jsonc
|
|
82
85
|
{
|
|
83
|
-
"image": "container/image
|
|
86
|
+
"image": "container/image@sha256:<64 lower-case hex>",
|
|
84
87
|
"port": 8080,
|
|
85
88
|
"envVars": [
|
|
86
89
|
{
|
|
@@ -125,8 +128,12 @@ Secret declarations never contain values. Each declares exactly one `file` or
|
|
|
125
128
|
required by upstream images; secret and platform-owned environment identities
|
|
126
129
|
remain uppercase canonical keys.
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
Every image is digest-pinned (`name@sha256:…`); the catalog contract refuses a
|
|
132
|
+
tag-only image.
|
|
133
|
+
|
|
134
|
+
`platformRequirements.s3: true` and `volumes` are legacy inputs.
|
|
135
|
+
`normalizeAppStoreStorageConfig` normalizes them deterministically for existing
|
|
136
|
+
templates, but new templates use
|
|
130
137
|
portable, template-local `storageClasses` and stable named `storageRequests`.
|
|
131
138
|
Legacy `platformRequirements.s3: true` cannot coexist with an explicit
|
|
132
139
|
`objectStorage` request. Legacy volume objects may retain `name`, `mountPath`,
|
|
@@ -253,21 +260,20 @@ identities, host paths, drivers, and driver options are rejected instead of
|
|
|
253
260
|
being passed through. Unknown `storage.*` feature IDs also fail closed. Request
|
|
254
261
|
IDs are stable across upgrades, restores, and migrations.
|
|
255
262
|
|
|
256
|
-
|
|
257
|
-
|
|
263
|
+
Every version config is judged by the catalog contract of `@serve.zone/interfaces`
|
|
264
|
+
(`appstore.validateAppStoreCatalog`, which applies `appstore.validateAppStoreVersionConfig`
|
|
265
|
+
to every config); this package has no validator of its own. Before provisioning, a
|
|
266
|
+
runtime advertises its supported feature IDs and calls the fail-closed normalization
|
|
267
|
+
entry point:
|
|
258
268
|
|
|
259
269
|
```ts
|
|
260
|
-
|
|
270
|
+
import { normalizeAppStoreStorageConfig } from '@serve.zone/appstore';
|
|
271
|
+
|
|
272
|
+
const normalizedStorage = normalizeAppStoreStorageConfig(catalogVersion.config, {
|
|
261
273
|
supportedFeatures: capabilityAdvertisement.featureIds,
|
|
262
274
|
});
|
|
263
275
|
```
|
|
264
276
|
|
|
265
|
-
The package also exports the pure `validateAppStoreStorageConfig()` and
|
|
266
|
-
`normalizeAppStoreStorageConfig()` functions, `parseStorageCapacityQuantity()`,
|
|
267
|
-
and the normalized storage handoff types. Structural validation is useful for
|
|
268
|
-
catalog ingestion; only normalization with a target's `supportedFeatures` is a
|
|
269
|
-
valid pre-fulfillment gate.
|
|
270
|
-
|
|
271
277
|
The result contains canonical `storageClasses`, `storageRequests`,
|
|
272
278
|
`requestOrigins`, and `requiredFeatureIds`. `requestOrigins` marks explicit,
|
|
273
279
|
legacy-volume, and legacy-S3 requests so fulfillment adapters can preserve old
|
|
@@ -281,79 +287,142 @@ unavailable.
|
|
|
281
287
|
|
|
282
288
|
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`.
|
|
283
289
|
|
|
284
|
-
|
|
290
|
+
## The Runtime Catalog
|
|
285
291
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
292
|
+
The catalog is one document, `IAppStoreCatalog` of the `@serve.zone/interfaces` App Store catalog contract: every app, every published version and every version's full config with its `configDigest`, plus `contract`, `revision`, `publishedAt` and `sourceRevision`. A release ships it as an `IAppStoreCatalogEnvelope`: the catalog, its `catalogDigest` and one or more Ed25519 signatures.
|
|
293
|
+
|
|
294
|
+
- `revision` is `major × 10¹² + minor × 10⁶ + patch` of the package version (`33.0.0` → `33000000000000`). Package versions only grow, so every release's catalog has a greater revision than the one before, and a consumer refuses any catalog whose revision is not greater than its active one (`revisionRegressed`). A prerelease version is refused. A catalog published from an older release line therefore never replaces a newer one.
|
|
295
|
+
- `sourceRevision` is the commit the catalog was built from and `publishedAt` that commit's committer time, so the same commit always builds the same catalog.
|
|
296
|
+
- Apps are ordered by id; versions in the order `app.json` lists them.
|
|
297
|
+
- A published `app@version` never changes and never disappears. A version is retired by withdrawing it (`withdrawn.json`); it stays listed with its config and is refused as an install or upgrade target. `catalog.lock.json` records every published version, and the build and the test suite refuse a catalog that removes, rewrites or reinstates one. Consumers apply the same rules between two catalogs (`publishedVersionRemoved`, `publishedVersionMutated`, `publishedWithdrawalChanged`).
|
|
298
|
+
|
|
299
|
+
## Publishing A Catalog
|
|
300
|
+
|
|
301
|
+
| Command | What it does |
|
|
302
|
+
| --- | --- |
|
|
303
|
+
| `pnpm build` | Compiles, then writes `dist_catalog/catalog.json` and `dist_catalog/catalog.digest`. Signs as well when `SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE` is set. |
|
|
304
|
+
| `pnpm run build:release` | The release build (`.smartconfig.json` `release.preflight.buildCommand`). Refuses a dirty working tree, any version `catalog.lock.json` does not record, and a missing signing key; writes the signed `dist_catalog/catalog.envelope.json`. |
|
|
305
|
+
| `pnpm run catalog:lock` | Records every new version (and every new withdrawal) in `catalog.lock.json`; refuses a catalog that violates it. Run it, and commit the lock, with every new version or withdrawal. |
|
|
306
|
+
| `pnpm run catalog:trustedkey` | Prints the `{ keyId, publicKey }` record consumers ship for the configured signing key. |
|
|
307
|
+
|
|
308
|
+
`SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE` names a file holding the PKCS#8 PEM of an Ed25519 private key (`openssl genpkey -algorithm ed25519`); the key itself never appears in arguments or logs. A release refuses to ship without it twice: `build:release` fails, and the package exports `./catalog.envelope.json`, so gitzone's entry-point check refuses a tarball that does not carry the envelope.
|
|
309
|
+
|
|
310
|
+
### Signing Key And Rotation
|
|
311
|
+
|
|
312
|
+
A release signs with exactly one key: the one `SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE` names. Consumers ship the trusted keys (`IAppStoreCatalogTrustedKey`, printed by `pnpm run catalog:trustedkey`) with their own release, and an envelope verifies when one of its signatures is by a key the consumer trusts. Rotating the key is therefore trust first, switch second:
|
|
306
313
|
|
|
307
|
-
|
|
314
|
+
1. Generate the new key and print its trusted-key record.
|
|
315
|
+
2. Release Onebox and Cloudly trusting both the current and the new key. Catalogs signed with the current key keep activating everywhere.
|
|
316
|
+
3. Once every running consumer trusts the new key, point the variable at the new key file for the next App Store release. A consumer that still trusts only the old key refuses that catalog (`signatureInvalid`, no signature by a trusted key) and keeps its last-known-good catalog until it is upgraded.
|
|
317
|
+
4. Release Onebox and Cloudly without the old key.
|
|
308
318
|
|
|
309
|
-
|
|
319
|
+
A compromised key follows the same steps, with step 2 dropping the compromised key at once: consumers then refuse every catalog until the first one signed with the new key is published. The contract allows up to eight signatures per envelope, but this publisher signs with one key; signing with two keys at once is not supported.
|
|
310
320
|
|
|
311
|
-
|
|
321
|
+
### Signing Key Custody
|
|
322
|
+
|
|
323
|
+
The signing key is a key file on the release machine, outside every repository, held by the release operator; nothing in this repository, its CI or its logs carries it. A release runs with `SERVEZONE_APPSTORE_CATALOG_SIGNING_KEY_FILE` naming that file:
|
|
324
|
+
|
|
325
|
+
- gitzone passes its environment to the release build, which it runs as `pnpm run build:release` in a clean checkout of the release commit; the publisher reads the file (`importAppStoreCatalogSigningKeyPem` of `@serve.zone/interfaces/runtime`), signs the catalog, verifies the envelope against the key's own trusted-key record and writes `dist_catalog/catalog.envelope.json` into the package gitzone publishes. It logs the key id and public key only.
|
|
326
|
+
- The Gitea tag workflow (`.gitea/workflows/release.yml`) has no key: it builds the unsigned catalog of the tag and attaches `catalog.json` and `catalog.digest`, whose digest equals the published envelope's `catalogDigest`.
|
|
327
|
+
- Generate a key with `openssl genpkey -algorithm ed25519 -out <key file>` on the release machine and keep the file readable by the release user only; `pnpm run catalog:trustedkey` with the variable set prints the record Onebox and Cloudly ship. Losing the file means rotating to a new key as above.
|
|
328
|
+
|
|
329
|
+
## Loading The Catalog
|
|
312
330
|
|
|
313
331
|
```ts
|
|
314
|
-
import {
|
|
332
|
+
import { AppStoreCatalogLoader } from '@serve.zone/appstore';
|
|
333
|
+
|
|
334
|
+
const loader = new AppStoreCatalogLoader({
|
|
335
|
+
trustedKeys, // IAppStoreCatalogTrustedKey[] shipped with this release
|
|
336
|
+
supportedFeatureIds, // the requiresFeatures ids this consumer fulfils
|
|
337
|
+
registries: configuredRegistries, // empty or absent: the default, npmjs
|
|
338
|
+
requestTimeoutMs: 30_000, // per request, body included; the default
|
|
339
|
+
});
|
|
315
340
|
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
341
|
+
// At start: the stored last-known-good envelope, verified again against today's trust.
|
|
342
|
+
const restored = await loader.restoreEnvelopeText(storedText, { origin: storedOrigin, loadedAt: storedLoadedAt });
|
|
343
|
+
// 'restored' activate restored.envelope; status.active = restored.active
|
|
344
|
+
// 'refused' no active catalog: status unavailable, cause 'refused', refused = restored.refused
|
|
345
|
+
|
|
346
|
+
// On the consumer's schedule, never awaited by startup or a request:
|
|
347
|
+
const result = await loader.loadFromRegistries(activeEnvelope ?? null, { signal: stopController.signal });
|
|
348
|
+
// 'activated' store result.envelope as last-known-good; status.active = result.active
|
|
349
|
+
// 'unchanged' nothing to store
|
|
350
|
+
// 'refused' keep the active catalog; status.refused = result.refused
|
|
351
|
+
// 'failed' keep the active catalog; retry with backoff
|
|
352
|
+
// Always: status.lastAttempt = result.attempt; result.registries reports each registry's answer.
|
|
353
|
+
// An aborted signal rejects the load with its reason: the consumer is stopping, nothing is recorded.
|
|
320
354
|
```
|
|
321
355
|
|
|
322
|
-
|
|
356
|
+
One load asks every configured registry for the package its `latest` dist-tag names, downloads the tarball (bounded in time and size, its unpacked size bounded too), checks it against the registry's `dist.integrity`, extracts `dist_catalog/catalog.envelope.json`, verifies it with `verifyAppStoreCatalogEnvelope` (shape, digest, a signature by a trusted key, contract, schema with the consumer's `supportedFeatureIds`, every config digest) and judges it against the active catalog with `judgeAppStoreCatalogSuccession`. The first registry whose catalog activates ends the load; otherwise the result is the first `unchanged`, else the first `refused`, else `failed`. Transport, integrity and packaging failures and every refusal are results, never exceptions; an envelope the verifier cannot read is refused as `schemaInvalid`.
|
|
323
357
|
|
|
324
|
-
|
|
358
|
+
The registry list follows the one rule of `@serve.zone/interfaces`: `appstore.validateAppStoreCatalogRegistries` (`https:` base URLs without credentials, query or fragment, each registry once) and `appstore.resolveAppStoreCatalogRegistries` (an empty list means `appStoreCatalogDefaultRegistries`). A consumer validates an operator's setting with the same function before it stores it.
|
|
325
359
|
|
|
326
|
-
|
|
360
|
+
The loader holds the consumer's options and nothing else: no catalog, no timer, no schedule, no persistence. The consumer's catalog runtime owns the one schedule of its process. The cadence Onebox and Cloudly follow, as guidance:
|
|
327
361
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
362
|
+
- restore the stored envelope at start and start the first load in the background, without waiting for it;
|
|
363
|
+
- load every 15 minutes while the last attempt activated or answered `unchanged` or `refused`;
|
|
364
|
+
- after a `failed` attempt, retry after 1 minute, doubling to at most 30 minutes, and return to 15 minutes after the next success;
|
|
365
|
+
- run one load at a time, abort it with the signal on stop, and answer an administrator's refresh with one load now.
|
|
366
|
+
|
|
367
|
+
An operator-imported envelope (an offline signed bundle, or the envelope a package carries at `@serve.zone/appstore/catalog.envelope.json`) is judged the same way with `judgeImportedEnvelope(value, active)` or `judgeImportedEnvelopeText(text, active)`; its activation reports `origin: 'operatorImport'`.
|
|
368
|
+
|
|
369
|
+
### Showing The Catalog
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
import { getAppStoreCatalogListing, projectAppStoreCatalogApp } from '@serve.zone/appstore';
|
|
373
|
+
|
|
374
|
+
const consumer = { supportedFeatureIds, consumer: { kind: 'onebox', version: oneboxVersion } };
|
|
375
|
+
const apps = getAppStoreCatalogListing(activeCatalog, consumer); // IAppStoreApp[] for getAppStoreTemplates
|
|
376
|
+
const projection = projectAppStoreCatalogApp(catalogApp, consumer);
|
|
377
|
+
// projection.versions: every listed version, each with installRefusal (null, versionWithdrawn,
|
|
378
|
+
// featuresUnsupported or consumerVersionTooOld)
|
|
379
|
+
// projection.latestInstallableVersion the newest version this consumer may install, or null
|
|
380
|
+
// projection.display { app: IAppStoreApp, meta: IAppStoreAppMeta }, or null without an install target
|
|
333
381
|
```
|
|
334
382
|
|
|
383
|
+
The display shapes list only the versions this consumer may install in `versions` and name the newest of them as `latestVersion`, so a version picker built from them never offers a withdrawn version, one gated on a feature the consumer lacks or one that needs a newer consumer release; `versionStates` states every listed version with its refusal. Under `semver` the newest install target is the highest one; under `branch` it is the catalog's `latestVersion` when that is an install target, and otherwise the app has none.
|
|
384
|
+
|
|
385
|
+
### Testing A Consumer
|
|
386
|
+
|
|
387
|
+
`@serve.zone/appstore/testing` serves a consumer's tests, never its runtime: `createAppStoreTestSigningKey` makes a throwaway key and its trusted-key record, `createAppStoreTestCatalog` and `createAppStoreTestVersionConfig` a catalog the contract accepts, `signAppStoreTestCatalog` its envelope, `createAppStoreTestPackageTarball` the package tarball carrying it, `createAppStoreTestRegistry` a registry stub and `createAppStoreTestFetch` the `fetch` a loader under test is given.
|
|
388
|
+
|
|
389
|
+
The published Cloudly templates are qualified here (`test/test.cloudlytemplates.node.ts`: pinned image, Onebox minimum, upgrade flags and platform OIDC of every template), so a template release needs no host test change.
|
|
390
|
+
|
|
391
|
+
## Migrating From 32.x
|
|
392
|
+
|
|
393
|
+
- `AppStoreResolver` and its `package` and `remote` source modes are gone, with the bundled `data.generated.ts`, `appStorePackageMetadata`, `appStoreDefaultRemoteBaseUrl` and `getSourceInfo()`. Load the signed catalog with `AppStoreCatalogLoader`, keep the last verified envelope in your database (restore it with `restoreEnvelopeText`), and read apps and versions from it (`findAppStoreCatalogApp`, `findAppStoreCatalogVersion`). Snapshot a service's version config and `configDigest` at install and upgrade; runtime paths read that snapshot, not the catalog.
|
|
394
|
+
- Linked `servezone.appstore.json` manifests (`source.type=repoManifest`), `dockerImage` sources and digest tracking (`upgradeStrategy: 'dockerDigest'`) are gone: every version is a catalog entry with a digest-pinned image. `parseAppStoreIndex`, `parseServezoneAppStoreManifest`, `parseDockerImageReference` and `createAppStoreVersionForDockerSource` are removed.
|
|
395
|
+
- `resolver.getApps()` is `getAppStoreCatalogListing(catalog, { supportedFeatureIds, consumer })` and `resolver.getAppMeta(appId)` is `projectAppStoreCatalogApp(app, { supportedFeatureIds, consumer }).display?.meta`; `resolver.getAppVersionConfig(appId, version)` is `findAppStoreCatalogVersion(catalog, appId, version)?.config`.
|
|
396
|
+
- Executable `migrate-from-*.ts` scripts are no longer supported; a version folder holds only `config.json` and `withdrawn.json`.
|
|
397
|
+
- Validation is owned by `@serve.zone/interfaces`: use `appstore.validateAppStoreVersionConfig` instead of `resolver.validateAppStoreVersionConfig`, and the storage contract instead of `validateAppStoreStorageConfig` and `parseStorageCapacityQuantity`.
|
|
398
|
+
- `resolver.normalizeStorageConfig(config, options, label)` is `normalizeAppStoreStorageConfig(config, options, label)`, and `resolver.normalizeVolumes(volumes)` is `normalizeAppStoreVolumes(config, label)`; both read the catalog version config shape (`appstore.TAppStoreCatalogVersionConfig`).
|
|
399
|
+
- The package no longer ships `apps/`, `appstore.json` or `appstore.resolved.json`; `appstore.json` is folded into each `app.json`.
|
|
400
|
+
|
|
335
401
|
## Working With Templates
|
|
336
402
|
|
|
337
|
-
-
|
|
338
|
-
-
|
|
403
|
+
- A new version is a new `apps/<app>/versions/<version>/config.json`, listed in `app.json` `versions` (and `latestVersion` when it is the highest). Never edit a published version: publish a new one, and withdraw the old one with `withdrawn.json` when it must not be installed any more.
|
|
404
|
+
- Pin every image to the digest of its multi-platform index (`name@sha256:…`) and record the tag and digest in the changelog.
|
|
405
|
+
- Run `pnpm run catalog:lock` and commit `catalog.lock.json` with every new version or withdrawal; the test suite fails on an unlocked or changed version.
|
|
406
|
+
- Gate a version that uses a config member newer than the catalog contract's minor on a feature id in `requiresFeatures`, so older consumers keep loading the rest of the catalog.
|
|
339
407
|
- Prefer explicit environment variable descriptions because Onebox surfaces them to users during installation.
|
|
340
408
|
- Use platform placeholders such as `${MARIADB_HOST}`, `${MARIADB_PORT}`, `${MARIADB_DATABASE}`, `${MARIADB_USER}`, `${MARIADB_PASSWORD}`, `${MONGODB_URI}`, `${S3_BUCKET}`, `${S3_ACCESS_KEY}`, `${S3_SECRET_KEY}`, `${MAIL_FROM}`, `${SMTP_HOST}`, `${SMTP_PORT}`, `${SMTP_USERNAME}`, and `${SMTP_PASSWORD}` only when the matching platform service is declared.
|
|
341
409
|
- Use named `objectStorage` requests instead of `platformRequirements.s3` in new templates, with a distinct delivery destination for every binding.
|
|
342
410
|
- 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.
|
|
343
|
-
- Keep template images boring and operationally safe unless there is a reason to pin a more specific upstream tag.
|
|
344
411
|
- Validate non-trivial templates by installing them through Onebox instead of only checking JSON syntax.
|
|
345
412
|
|
|
346
413
|
## Project Map
|
|
347
414
|
|
|
348
415
|
```text
|
|
349
416
|
appstore/
|
|
350
|
-
├──
|
|
351
|
-
├──
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
417
|
+
├── apps/
|
|
418
|
+
│ ├── cloudly/
|
|
419
|
+
│ │ ├── app.json
|
|
420
|
+
│ │ └── versions/<version>/config.json (+ withdrawn.json)
|
|
421
|
+
│ └── ...
|
|
422
|
+
├── catalog.lock.json
|
|
423
|
+
├── ts_publisher/ catalog builder, signing, lock (not published)
|
|
424
|
+
├── ts_client/ AppStoreCatalogLoader, catalog lookups, storage handoff
|
|
425
|
+
└── dist_catalog/ build output: catalog.json, catalog.digest, catalog.envelope.json
|
|
357
426
|
```
|
|
358
427
|
|
|
359
428
|
## License and Legal Information
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
type TCatalog = plugins.servezoneInterfaces.appstore.IAppStoreCatalog;
|
|
4
|
+
type TCatalogApp = plugins.servezoneInterfaces.appstore.IAppStoreCatalogApp;
|
|
5
|
+
type TCatalogVersion = plugins.servezoneInterfaces.appstore.IAppStoreCatalogVersion;
|
|
6
|
+
|
|
7
|
+
/** The npm package that carries the signed App Store catalog. */
|
|
8
|
+
export const appStoreCatalogPackageName = '@serve.zone/appstore';
|
|
9
|
+
|
|
10
|
+
/** Where the signed envelope sits inside the published package (and its `./catalog.envelope.json` export). */
|
|
11
|
+
export const appStoreCatalogEnvelopePackagePath = 'dist_catalog/catalog.envelope.json';
|
|
12
|
+
|
|
13
|
+
/** The app a catalog lists under `appIdArg`. */
|
|
14
|
+
export const findAppStoreCatalogApp = (catalogArg: TCatalog, appIdArg: string): TCatalogApp | undefined =>
|
|
15
|
+
catalogArg.apps.find((appArg) => appArg.info.id === appIdArg);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The listed `app@version`, withdrawn ones included: an installed service keeps reading its
|
|
19
|
+
* version. Whether it is an install or upgrade target is `appstore.judgeAppStoreCatalogInstallTarget`.
|
|
20
|
+
*/
|
|
21
|
+
export const findAppStoreCatalogVersion = (
|
|
22
|
+
catalogArg: TCatalog,
|
|
23
|
+
appIdArg: string,
|
|
24
|
+
versionArg: string,
|
|
25
|
+
): TCatalogVersion | undefined =>
|
|
26
|
+
findAppStoreCatalogApp(catalogArg, appIdArg)?.versions.find((entryArg) => entryArg.version === versionArg);
|
package/ts_client/index.ts
CHANGED