@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
package/readme.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @serve.zone/appstore
2
2
 
3
- This repository is the curated App Store index consumed by serve.zone runtimes. It publishes `@serve.zone/appstore`, a small TypeScript client that parses and resolves App Store metadata, linked `servezone.appstore.json` manifests, and Docker digest-tracked image sources.
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
- | `appstore.json` | Top-level App Store index with app IDs, display names, descriptions, categories, icons, latest versions, and tags. |
22
- | `apps/<app>/app.json` | App-specific metadata, maintained version list, and optional links. |
23
- | `apps/<app>/versions/<version>/config.json` | Deployable version definition: image, internal port, environment variables, platform requirements, and compatibility metadata. |
24
- | `ts_client/` | Published `@serve.zone/appstore` parser and resolver client. |
25
- | `source.type=repoManifest` | Optional link to a `servezone.appstore.json` file owned by the upstream app repo. |
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
- Changes are reviewed by reading the JSON data, running the client tests, and testing affected templates in Onebox or Cloudly.
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 App Store currently lists 21 app templates.
32
-
33
- | App ID | Name | App Store category | Image | Port |
34
- | --- | --- | --- | --- | --- |
35
- | `adminer` | Adminer | Dev Tools | `adminer:latest` | `8080` |
36
- | `cloudly` | Cloudly | Dev Tools | `code.foss.global/serve.zone/cloudly@sha256:700bb723fef408a6bf8079f643f9990247bffe6541403fd421d064ba8128a447` | `3000` |
37
- | `ghost` | Ghost | CMS | `ghost:latest` | `2368` |
38
- | `gitea` | Gitea | Dev Tools | `gitea/gitea:latest` | `3000` |
39
- | `gitops` | GitOps | Dev Tools | `code.foss.global/serve.zone/gitops:2.13.0` | `3000` |
40
- | `grafana` | Grafana | Monitoring | `grafana/grafana:latest` | `3000` |
41
- | `mariadb` | MariaDB | Database | `mariadb:latest` | `3306` |
42
- | `mattermost` | Mattermost | Communication | `mattermost/mattermost-team-edition:latest` | `8065` |
43
- | `n8n` | N8N | Automation | `n8nio/n8n:latest` | `5678` |
44
- | `nextcloud` | Nextcloud | Storage | `nextcloud:latest` | `80` |
45
- | `nginx` | Nginx | Web Server | `nginx:alpine` | `80` |
46
- | `openbao` | OpenBao | Security | `openbao/openbao:2.6.0` | `8200` |
47
- | `plausible` | Plausible Analytics | Analytics | `plausible/analytics:latest` | `8000` |
48
- | `portainer` | Portainer | Dev Tools | `portainer/portainer-ce:latest` | `9000` |
49
- | `postgres` | PostgreSQL | Database | `postgres:16-alpine` | `5432` |
50
- | `valkey` | Valkey | Database | `valkey/valkey:9.1.0-alpine` | `6379` |
51
- | `rustdesk-server` | RustDesk Server | Remote Access | `rustdesk/rustdesk-server-s6:latest` | `21116` |
52
- | `siprouter` | SIP Router | Communication | `code.foss.global/serve.zone/siprouter:1.28.0` | `3060` |
53
- | `uptime-kuma` | Uptime Kuma | Monitoring | `louislam/uptime-kuma:latest` | `3001` |
54
- | `vaultwarden` | Vaultwarden | Security | `vaultwarden/server:latest` | `80` |
55
- | `wordpress` | WordPress | CMS | `wordpress:latest` | `80` |
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:tag",
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
- `platformRequirements.s3: true` and `volumes` are legacy inputs. The resolver
129
- normalizes them deterministically for existing templates, but new templates use
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
- Before provisioning, a runtime must advertise its supported feature IDs and
257
- call the resolver's fail-closed normalization entry point:
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
- const normalizedStorage = resolver.normalizeStorageConfig(config, {
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
- Linked app manifests use the same runtime schema and let the upstream app repo own its install and upgrade metadata:
290
+ ## The Runtime Catalog
285
291
 
286
- ```jsonc
287
- {
288
- "app": {
289
- "id": "cloudly",
290
- "name": "Cloudly",
291
- "description": "Multi-node serve.zone control plane.",
292
- "category": "Dev Tools"
293
- },
294
- "latestVersion": "latest",
295
- "source": {
296
- "type": "dockerImage",
297
- "image": "code.foss.global/serve.zone/cloudly:latest",
298
- "tracking": "digest"
299
- },
300
- "runtime": {
301
- "image": "code.foss.global/serve.zone/cloudly:latest",
302
- "port": 3000
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
- When `tracking` is `digest`, Onebox resolves the Docker manifest digest and treats digest changes as App Store upgrades while keeping the image reference simple for deployment.
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
- Neither the index nor a linked manifest states a schema version. Published documents that still carry a `schemaVersion` member are read unchanged — the readers ignore it — so a catalog can be republished without the member at any time.
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
- ## Client Usage
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 { AppStoreResolver } from '@serve.zone/appstore';
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
- const resolver = new AppStoreResolver();
317
- const appStore = await resolver.getAppStoreIndex();
318
- const cloudly = await resolver.getAppMeta('cloudly');
319
- const config = await resolver.getAppVersionConfig('cloudly', cloudly.latestVersion);
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
- The resolver defaults to the App Store data bundled in the published `@serve.zone/appstore` package. Tests and explicit remote consumers can inject a custom `fetch` implementation and base URL. Remote operations default to a 30-second timeout, configurable with a positive-integer `requestTimeoutMs`.
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
- Catalog resolution fails closed when index and app metadata IDs or latest versions disagree, when metadata latest is absent from its version list, or when an all-semver list does not select its highest semantic value. Repo manifests with multiple non-semver versions require explicit latest or channel intent; array position never determines latest.
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
- Only include keys that are actually needed by a template. For example, `nginx` is currently just:
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
- ```json
329
- {
330
- "image": "nginx:alpine",
331
- "port": 80
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
- - Keep `appstore.json`, `apps/<app>/app.json`, and each version config in sync when adding or changing an app.
338
- - 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.
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
- ├── appstore.json
351
- ├── ts_client/
352
- └── apps/
353
- ├── cloudly/
354
- │ ├── app.json
355
- │ └── versions/<version>/config.json
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);
@@ -1,3 +1,5 @@
1
- export * from './classes.appstoreresolver.js';
1
+ export * from './catalog.js';
2
+ export * from './loader.js';
3
+ export * from './projection.js';
2
4
  export * from './storage.js';
3
5
  export * from './types.js';