@serve.zone/interfaces 32.52.0 → 32.54.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 (106) hide show
  1. package/changelog.md +73 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/appstore/catalog.d.ts +92 -12
  4. package/dist_ts/appstore/catalog.js +119 -5
  5. package/dist_ts/appstore/types.d.ts +28 -4
  6. package/dist_ts/appstore/types.js +1 -1
  7. package/dist_ts/appstore/versionconfig.d.ts +1 -1
  8. package/dist_ts/data/cluster.d.ts +1 -1
  9. package/dist_ts/data/clusteringress.d.ts +4 -4
  10. package/dist_ts/data/clusteringressdesignation.d.ts +2 -2
  11. package/dist_ts/data/clusternode.d.ts +6 -6
  12. package/dist_ts/data/clusternode.js +1 -1
  13. package/dist_ts/data/clusternodehostname.d.ts +5 -5
  14. package/dist_ts/data/clusternodehostname.js +3 -3
  15. package/dist_ts/data/clusterrelaycredential.d.ts +3 -3
  16. package/dist_ts/data/clusterruntime.d.ts +1 -1
  17. package/dist_ts/data/clustervpn.d.ts +4 -4
  18. package/dist_ts/data/coremail.d.ts +11 -11
  19. package/dist_ts/data/coremail.runtime.d.ts +6 -6
  20. package/dist_ts/data/coremail.runtime.js +1 -1
  21. package/dist_ts/data/corestorecontroltoken.d.ts +1 -1
  22. package/dist_ts/data/corestorelegacyadoption.d.ts +2 -2
  23. package/dist_ts/data/deploymentmachineuser.d.ts +1 -1
  24. package/dist_ts/data/deploymentpreflight.d.ts +26 -1
  25. package/dist_ts/data/fleetcutover.d.ts +28 -28
  26. package/dist_ts/data/hostedapp.d.ts +4 -4
  27. package/dist_ts/data/immutableimage.d.ts +3 -3
  28. package/dist_ts/data/isolatedrestore.d.ts +7 -7
  29. package/dist_ts/data/isolatedrestore.js +1 -1
  30. package/dist_ts/data/networkaddressplan.d.ts +2 -2
  31. package/dist_ts/data/networkapplication.d.ts +9 -9
  32. package/dist_ts/data/networkauthority.d.ts +5 -5
  33. package/dist_ts/data/networkcredential.js +1 -1
  34. package/dist_ts/data/networkegressretirement.d.ts +3 -3
  35. package/dist_ts/data/networkegressretirement.js +1 -1
  36. package/dist_ts/data/networkhost.d.ts +3 -3
  37. package/dist_ts/data/networklease.d.ts +9 -9
  38. package/dist_ts/data/networkprojection.d.ts +7 -7
  39. package/dist_ts/data/networkprojectionretirement.d.ts +6 -6
  40. package/dist_ts/data/networkprotection.d.ts +9 -9
  41. package/dist_ts/data/networksigning.d.ts +6 -6
  42. package/dist_ts/data/networkworkloadingress.d.ts +3 -3
  43. package/dist_ts/data/nodeenrollment.d.ts +8 -8
  44. package/dist_ts/data/noderetirement.d.ts +1 -1
  45. package/dist_ts/data/noderuntimebinding.d.ts +11 -11
  46. package/dist_ts/data/privatenetwork.d.ts +3 -3
  47. package/dist_ts/data/runtimeassignment.d.ts +9 -9
  48. package/dist_ts/data/runtimeassignment.js +1 -1
  49. package/dist_ts/data/runtimeassignmentexec.d.ts +2 -2
  50. package/dist_ts/data/runtimeassignmentlogs.d.ts +4 -4
  51. package/dist_ts/data/runtimeassignmentstats.d.ts +2 -2
  52. package/dist_ts/data/runtimeconfig.d.ts +1 -1
  53. package/dist_ts/data/runtimelocalstorageclaim.d.ts +4 -4
  54. package/dist_ts/data/runtimelocalvolumearchive.d.ts +5 -5
  55. package/dist_ts/data/runtimesession.d.ts +5 -5
  56. package/dist_ts/data/runtimesession.js +1 -1
  57. package/dist_ts/data/runtimestorageclaim.d.ts +4 -4
  58. package/dist_ts/data/runtimeterminalreceipt.d.ts +2 -2
  59. package/dist_ts/data/runtimeterminalreceipt.js +1 -1
  60. package/dist_ts/data/secret.d.ts +4 -4
  61. package/dist_ts/data/secret.js +1 -1
  62. package/dist_ts/data/service.d.ts +10 -3
  63. package/dist_ts/data/serviceplatformownership.d.ts +1 -1
  64. package/dist_ts/data/serviceports.js +1 -1
  65. package/dist_ts/data/serviceruntime.d.ts +3 -3
  66. package/dist_ts/data/servicevolumeinitialization.d.ts +1 -1
  67. package/dist_ts/data/settings.d.ts +37 -31
  68. package/dist_ts/data/settings.js +1 -1
  69. package/dist_ts/data/sparklegacyretirement.d.ts +4 -4
  70. package/dist_ts/data/sparknodereport.d.ts +2 -2
  71. package/dist_ts/platform/storage.d.ts +1 -1
  72. package/dist_ts/platform/storagemigration.d.ts +6 -6
  73. package/dist_ts/platform/storagemigration.golden.d.ts +13 -13
  74. package/dist_ts/platform/types.d.ts +1 -1
  75. package/dist_ts/private/canonicaljson.d.ts +6 -6
  76. package/dist_ts/private/canonicaljson.js +1 -1
  77. package/dist_ts/private/dataproperties.js +1 -1
  78. package/dist_ts/private/networkcontract.js +1 -1
  79. package/dist_ts/private/storagecontract.d.ts +1 -1
  80. package/dist_ts/private/storagecontract.js +1 -1
  81. package/dist_ts/protocol.d.ts +1 -1
  82. package/dist_ts/requests/appstore.d.ts +18 -0
  83. package/dist_ts/requests/hostedapp.d.ts +6 -0
  84. package/dist_ts/requests/hostedapp.js +1 -1
  85. package/dist_ts/requests/node.d.ts +1 -1
  86. package/dist_ts/requests/service.d.ts +23 -0
  87. package/dist_ts/runtime.appstorecatalog.d.ts +17 -0
  88. package/dist_ts/runtime.appstorecatalog.js +84 -12
  89. package/dist_ts/runtime.assignmentsecret.d.ts +3 -3
  90. package/dist_ts/runtime.assignmentsecret.js +1 -1
  91. package/dist_ts/runtime.corestore.d.ts +13 -13
  92. package/dist_ts/runtime.d.ts +1 -1
  93. package/dist_ts/runtime.workloadinit.d.ts +11 -11
  94. package/package.json +5 -4
  95. package/readme.md +10 -1
  96. package/ts/00_commitinfo_data.ts +1 -1
  97. package/ts/appstore/catalog.ts +175 -5
  98. package/ts/appstore/types.ts +25 -0
  99. package/ts/data/clusternodehostname.ts +3 -3
  100. package/ts/data/service.ts +10 -2
  101. package/ts/data/settings.ts +6 -0
  102. package/ts/requests/appstore.ts +22 -0
  103. package/ts/requests/hostedapp.ts +6 -0
  104. package/ts/requests/node.ts +1 -1
  105. package/ts/requests/service.ts +28 -0
  106. package/ts/runtime.appstorecatalog.ts +91 -11
@@ -5,13 +5,13 @@ import type { IObjectStorageRetentionIntent, IObjectStorageRetentionEvidenceExpe
5
5
  import type { IPlatformDatabaseEndpoint } from './platform/database.js';
6
6
  export type TCorestoreCredentialCapability = 'database' | 'objectstorage';
7
7
  export declare const corestoreProviderConfigIds: Readonly<{
8
- readonly database: "cloudly-corestore-database";
9
- readonly objectstorage: "cloudly-corestore-objectstorage";
8
+ readonly database: 'cloudly-corestore-database';
9
+ readonly objectstorage: 'cloudly-corestore-objectstorage';
10
10
  }>;
11
- export declare const corestoreControlUrl: "http://corestore:3000";
12
- export declare const corestoreCredentialManagementScope: "platform:cloudly-corestore";
13
- export declare const corestoreControlCredentialKey: "CORESTORE_API_TOKEN";
14
- export declare const corestoreCredentialEnvironment: "production";
11
+ export declare const corestoreControlUrl: 'http://corestore:3000';
12
+ export declare const corestoreCredentialManagementScope: 'platform:cloudly-corestore';
13
+ export declare const corestoreControlCredentialKey: 'CORESTORE_API_TOKEN';
14
+ export declare const corestoreCredentialEnvironment: 'production';
15
15
  export declare const corestoreCredentialRuntimeLimits: Readonly<{
16
16
  maximumIdentifierLength: 200;
17
17
  maximumPublicationGrants: 256;
@@ -22,7 +22,7 @@ export declare const corestoreCredentialRuntimeLimits: Readonly<{
22
22
  maximumControlTokenBytes: 4096;
23
23
  maximumSealedControlCredentialBytes: number;
24
24
  }>;
25
- export declare const getCorestoreProviderConfigId: (capabilityArg: TCorestoreCredentialCapability) => (typeof corestoreProviderConfigIds)[TCorestoreCredentialCapability];
25
+ export declare const getCorestoreProviderConfigId: (capabilityArg: TCorestoreCredentialCapability) => typeof corestoreProviderConfigIds[TCorestoreCredentialCapability];
26
26
  export interface ICorestoreControlCredentialPlaintext {
27
27
  key: typeof corestoreControlCredentialKey;
28
28
  environment: typeof corestoreCredentialEnvironment;
@@ -92,7 +92,7 @@ interface ICorestoreCredentialBindingRequestBase {
92
92
  managementScope: typeof corestoreCredentialManagementScope;
93
93
  environment: typeof corestoreCredentialEnvironment;
94
94
  }
95
- export type TCorestoreCredentialBindingRequest = ICorestoreCredentialBindingRequestBase & {
95
+ export type TCorestoreCredentialBindingRequest = (ICorestoreCredentialBindingRequestBase & {
96
96
  capability: 'database';
97
97
  /**
98
98
  * Where a workload reaches the database: the `corestoreDatabase` of the node the binding is
@@ -102,11 +102,11 @@ export type TCorestoreCredentialBindingRequest = ICorestoreCredentialBindingRequ
102
102
  * is admitted is the provider's decision, never a default this contract supplies.
103
103
  */
104
104
  endpoint?: IPlatformDatabaseEndpoint;
105
- } | ICorestoreCredentialBindingRequestBase & {
105
+ }) | (ICorestoreCredentialBindingRequestBase & {
106
106
  capability: 'objectstorage';
107
107
  bucketName: string;
108
108
  objectstorageRetention?: IObjectStorageRetentionIntent;
109
- };
109
+ });
110
110
  export declare const validateCorestoreCredentialBindingRequest: (requestArg: unknown) => string[];
111
111
  export declare const createCorestoreCredentialBindingRequestDigestInput: (requestArg: TCorestoreCredentialBindingRequest) => string;
112
112
  export declare const computeCorestoreCredentialBindingRequestSha256: (requestArg: TCorestoreCredentialBindingRequest) => Promise<string>;
@@ -209,7 +209,7 @@ export interface IReq_PublishCorestoreCredentialMaterial extends plugins.typedre
209
209
  receipt: ICorestoreCredentialPublicationReceipt;
210
210
  };
211
211
  }
212
- export declare const createPublishCorestoreCredentialMaterialEnvelopeContext: (requestArg: Omit<IPublishCorestoreCredentialMaterialRequest, "identity" | "envelope">) => Uint8Array;
212
+ export declare const createPublishCorestoreCredentialMaterialEnvelopeContext: (requestArg: Omit<IPublishCorestoreCredentialMaterialRequest, 'identity' | 'envelope'>) => Uint8Array;
213
213
  export declare const validatePublishCorestoreCredentialMaterialRequest: (requestArg: unknown, expectedGrantArg: ICorestoreCredentialPublicationGrant, activeRecipientArg: IActiveSecretRecipientMetadata, trustedNowArg: number) => Promise<string[]>;
214
214
  export interface ICorestoreCredentialPublicationReceiptExpectation {
215
215
  request: IPublishCorestoreCredentialMaterialRequest;
@@ -312,9 +312,9 @@ export interface ICorestoreDatabaseBackupClosureReceipt {
312
312
  * is a transfer that fails at the last hop instead of at the first. Every one of them imports this
313
313
  * constant rather than writing the bytes again.
314
314
  */
315
- export declare const corestoreDatabaseBackupMediaType: "application/vnd.serve-zone.corestore-database-backup";
315
+ export declare const corestoreDatabaseBackupMediaType: 'application/vnd.serve-zone.corestore-database-backup';
316
316
  /** The format a backup receipt states, which is this media type's payload contract. */
317
- export declare const corestoreDatabaseBackupFormat: "corestore.database.backup";
317
+ export declare const corestoreDatabaseBackupFormat: 'corestore.database.backup';
318
318
  export interface ICorestoreDatabaseBackupReceipt {
319
319
  format: typeof corestoreDatabaseBackupFormat;
320
320
  backupId: string;
@@ -144,7 +144,7 @@ export interface ICoreflowSecretRuntimeRegistrationExpectation {
144
144
  activeRecipient: IActiveSecretRecipientMetadata;
145
145
  }
146
146
  /** Dedicated TypedSocket tag carrying ICoreflowSecretRuntimeRegistration. */
147
- export declare const coreflowSecretRuntimeRegistrationTagId: "coreflowSecretRuntimeRegistration";
147
+ export declare const coreflowSecretRuntimeRegistrationTagId: 'coreflowSecretRuntimeRegistration';
148
148
  export type TCoreflowSecretRuntimeRegistrationExpectationResponse = {
149
149
  status: 'available';
150
150
  expectation: ICoreflowSecretRuntimeRegistrationExpectation;
@@ -1,16 +1,16 @@
1
1
  import type { TSha256Digest, TImmutableContainerPlatform } from './data/immutableimage.js';
2
2
  export declare const workloadInitReleaseContract: Readonly<{
3
- readonly packageName: "@serve.zone/workloadinit";
4
- readonly sourceRepository: "https://code.foss.global/serve.zone/workloadinit.git";
5
- readonly imageRegistry: "code.foss.global";
6
- readonly imageRepository: "serve.zone/workloadinit";
7
- readonly imageIndexMediaType: "application/vnd.oci.image.index.v1+json";
8
- readonly statementType: "https://in-toto.io/Statement/v1";
9
- readonly predicateType: "https://slsa.dev/provenance/v1";
10
- readonly buildType: "https://serve.zone/buildtypes/workloadinit-gitzone-tsdocker";
11
- readonly builderId: "https://serve.zone/builders/workloadinit-manual-release";
12
- readonly signatureFormat: "cosign-dsse-bundle-v0.3";
13
- readonly signatureAlgorithm: "ecdsa-p256-sha256";
3
+ readonly packageName: '@serve.zone/workloadinit';
4
+ readonly sourceRepository: 'https://code.foss.global/serve.zone/workloadinit.git';
5
+ readonly imageRegistry: 'code.foss.global';
6
+ readonly imageRepository: 'serve.zone/workloadinit';
7
+ readonly imageIndexMediaType: 'application/vnd.oci.image.index.v1+json';
8
+ readonly statementType: 'https://in-toto.io/Statement/v1';
9
+ readonly predicateType: 'https://slsa.dev/provenance/v1';
10
+ readonly buildType: 'https://serve.zone/buildtypes/workloadinit-gitzone-tsdocker';
11
+ readonly builderId: 'https://serve.zone/builders/workloadinit-manual-release';
12
+ readonly signatureFormat: 'cosign-dsse-bundle-v0.3';
13
+ readonly signatureAlgorithm: 'ecdsa-p256-sha256';
14
14
  }>;
15
15
  export interface IWorkloadInitBuildToolVersions {
16
16
  dockerBuildx: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "32.52.0",
3
+ "version": "32.54.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -27,11 +27,12 @@
27
27
  "@tsclass/tsclass": "^9.5.1"
28
28
  },
29
29
  "devDependencies": {
30
- "@git.zone/tsbuild": "^5.1.2",
30
+ "@git.zone/tsbuild": "^6.0.2",
31
31
  "@git.zone/tsdoc": "^3.0.2",
32
- "@git.zone/tsrun": "^3.0.2",
32
+ "@git.zone/tsrun": "^3.0.3",
33
33
  "@git.zone/tstest": "^7.2.0",
34
- "@types/node": "26.6.3"
34
+ "@types/node": "26.6.4",
35
+ "typescript": "^6.0.3"
35
36
  },
36
37
  "files": [
37
38
  "ts/**/*",
package/readme.md CHANGED
@@ -3559,6 +3559,7 @@ Compatibility rules:
3559
3559
  - Every level has exact keys; an unknown member is `schemaInvalid` (`appstore.validateAppStoreCatalog`).
3560
3560
  - `requiresFeatures` gates one version: a consumer passes the feature ids it fulfils (`supportedFeatureIds`, e.g. `appStoreStorageFeatureIds` values). A version naming any other id stays listed, is refused for install and upgrade there, and its config is not interpreted, so a new config capability ships under a feature id without refusing the rest of the catalog (`appstore.getAppStoreCatalogMissingFeatureIds`).
3561
3561
  - A published `app@version` stays in every later catalog and never changes (`appstore.judgeAppStoreCatalogSuccession`): a later catalog that no longer lists it is `publishedVersionRemoved`, one that lists it with another `configDigest` is `publishedVersionMutated`, and a revision that does not increase is `revisionRegressed`.
3562
+ - A version names the oldest host releases that run it (`minOneboxVersion`, `minCloudlyVersion`). A host passes itself to `appstore.judgeAppStoreCatalogInstallTarget` as `{ kind: 'onebox' | 'cloudly', version }` (`IAppStoreCatalogConsumer`), which refuses a version needing a newer release of it as `consumerVersionTooOld`; the version stays listed and installs once the host is upgraded.
3562
3563
  - A publisher retires a version by withdrawing it, never by removing it. A withdrawn version keeps its entry, config and digest, so services installed from it keep reading it, and is refused as an install or upgrade target, like a version gated on a feature the consumer lacks (`appstore.judgeAppStoreCatalogInstallTarget`). A withdrawal is final: a later catalog that revokes or rewrites it is `publishedWithdrawalChanged`; a version withdrawn by mistake is republished under a new version.
3563
3564
 
3564
3565
  Digests are SHA-256 over strict canonical JSON (keys in UTF-16 code-unit order, safe integers only) of a domain-separated value: `computeAppStoreCatalogConfigDigest`, `computeAppStoreCatalogDigest` and `computeAppStoreCatalogSigningKeyId`. A signature covers `createAppStoreCatalogSignaturePayload(catalogDigest)`. These helpers use Web Crypto and are safe in the browser entry.
@@ -3583,6 +3584,14 @@ if (!verification.accepted) {
3583
3584
 
3584
3585
  `verifyAppStoreCatalogEnvelope` authenticates before it interprets: envelope shape, `catalogDigest`, one valid signature by a trusted key (a failing signature by a trusted key refuses the envelope), then contract, schema and every `configDigest`. The consumer then judges succession against its active catalog.
3585
3586
 
3587
+ A publisher keeps its signing key as a file: `importAppStoreCatalogSigningKeyPem(text)` reads one unencrypted PKCS#8 PEM Ed25519 private key (`openssl genpkey -algorithm ed25519`) into an `IAppStoreCatalogSigningKey` with its public key and key id, and `exportAppStoreCatalogSigningKeyPem(key)` writes one. `getAppStoreCatalogTrustedKey(key)` is the `IAppStoreCatalogTrustedKey` a consumer ships for it. A trusted key also converts from and to an SPKI PEM public key (`openssl pkey -pubout`) with `importAppStoreCatalogTrustedKeyPem` and `exportAppStoreCatalogTrustedKeyPem`. Each refuses another PEM label or key type and a key id that does not match its public key, and no error quotes its input.
3588
+
3589
+ A service records the App Store version it runs as `IService.data.appStoreTemplate` (`IAppStoreServiceTemplateSnapshot`: `appId`, `version`, `configDigest`, `config`, `catalogRevision`), taken from a verified catalog by `appstore.createAppStoreServiceTemplateSnapshot`. It is server-managed (`TServiceWritableData` excludes it): the host writes it at install, upgrade and catalog adoption, and runtime paths read it, never the live catalog. While no catalog is active, an `unavailable` status states its `cause` (`loading`, `failed`, `refused`) for programs next to the operator's `reason`, and App Store install, upgrade and upgrade preview are refused with the retryable code `APPSTORE_CATALOG_UNAVAILABLE` (`IAppStoreCatalogUnavailableErrorData`). `IReq_Any_ImportAppStoreCatalog` imports an operator's envelope text (origin `operatorImport`) and answers the status after it, so an air-gapped host needs no registry.
3590
+
3591
+ A host loads the catalog from the npm registries `appstore.resolveAppStoreCatalogRegistries(list)` answers: the configured list (Cloudly's `ICloudlySettings.appStoreCatalogRegistries`, an Onebox setting) in canonical form, or `appStoreCatalogDefaultRegistries` (npmjs) when it is empty. `appstore.validateAppStoreCatalogRegistries` is the one rule for a configured list: `https:` base URLs without credentials, query or fragment, each registry once.
3592
+
3593
+ A host's App Store views carry the listing as `IAppStoreApp` and `IAppStoreAppMeta`: `versions` names the install targets, and the optional `versionStates` (`IAppStoreAppVersionState`) states every listed version, withdrawn and feature-gated ones included, with its `judgeAppStoreCatalogInstallTarget` answer as `installRefusal`.
3594
+
3586
3595
  `TAppStoreCatalogStatus` is the operator's view, discriminated by `availability`: `ready` (`IAppStoreCatalogReadyStatus`) always carries the `active` catalog's revision, digest, key and origin, including a restored last-known-good catalog; `unavailable` (`IAppStoreCatalogUnavailableStatus`) always carries a `reason` and no active catalog. Both state `supportedContract`, `supportedFeatureIds`, `lastAttempt`, `nextAttemptAt` and the last `refused` catalog. Loading a catalog never gates the boot, readiness or health of anything else; while none is active only App Store install, upgrade and upgrade-preview are refused. `requests.appstore.IReq_Any_GetAppStoreCatalogStatus` reads it, `IReq_Any_RefreshAppStoreCatalog` runs one attempt now and answers the state after it, and `IReq_Any_GetAppStoreTemplates` may answer it as `catalogStatus`.
3587
3596
 
3588
3597
  The catalog validator judges every supported version config in full; no consumer needs a second validator. Besides the catalog's own rules (member set, image pinning, the feature gate, minimum versions, upgrade flags, `healthCheck`) it applies `appstore.validateAppStoreVersionConfig`, the member rules a host applies before it installs a version, in this order:
@@ -3653,7 +3662,7 @@ The immutable deployment workflow is:
3653
3662
  1. Reserve the exact service, namespace, registry repository/tag, and route intent with `reserveServiceDeployment`.
3654
3663
  2. Push the OCI index to the returned exact tag with the authenticated deployer identity.
3655
3664
  3. Promote the authenticated release evidence with `promoteServiceImageRelease`.
3656
- 4. Observe exact rollout and runtime-digest evidence with `getServiceDeploymentStatus`.
3665
+ 4. Observe exact rollout and runtime-digest evidence with `getServiceDeploymentStatus`. That request is for a deployment polling its own rollout: Cloudly transitions a rolling-out operation to success or failure as its evidence proves either. An inspection, such as `tsdeploy deploy-status`, uses `readServiceDeploymentStatus` instead. It takes the same request and answers the same `IServiceDeploymentStatus`, but changes nothing.
3657
3666
  5. For a greenfield service, expose and verify its public route with `promoteServiceDeploymentRoute` only after the immutable rollout succeeds.
3658
3667
 
3659
3668
  A legacy `deployOnPush` service without deployable source moves to a greenfield successor by a legacy import, which replaces step 2:
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.52.0',
6
+ version: '32.54.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { TSha256Digest } from '../data/immutableimage.js';
14
14
  import { getDigestFromPinnedImageReference, isSha256Digest } from '../data/immutableimage.js';
15
+ import { compareReleaseVersions, parseReleaseVersion } from '../data/releaseversion.js';
15
16
  import {
16
17
  canonicalizeStrictJson,
17
18
  compareWireTokens,
@@ -268,14 +269,39 @@ export interface IAppStoreCatalogReadyStatus extends IAppStoreCatalogStatusBase
268
269
  reason?: never;
269
270
  }
270
271
 
272
+ /**
273
+ * Why no verified catalog is active, for programs; `reason` says it for operators.
274
+ *
275
+ * - `loading`: no attempt has ended since start and no last-known-good catalog was restored.
276
+ * - `failed`: the last attempt read no candidate (transport, integrity or packaging failed).
277
+ * - `refused`: the last attempt read a candidate and refused it (`refused` holds the record).
278
+ */
279
+ export type TAppStoreCatalogUnavailableCause = 'loading' | 'failed' | 'refused';
280
+
271
281
  /** No verified catalog is active, neither a last-known-good one nor a fetched one. */
272
282
  export interface IAppStoreCatalogUnavailableStatus extends IAppStoreCatalogStatusBase {
273
283
  availability: 'unavailable';
274
- /** Why no catalog is active, e.g. the last attempt's failure. */
284
+ /** Why no catalog is active, e.g. the last attempt's failure. For operators; never parse it. */
275
285
  reason: string;
286
+ /** Why no catalog is active, for programs. Every host that loads the catalog at runtime states it. */
287
+ cause?: TAppStoreCatalogUnavailableCause;
276
288
  active?: never;
277
289
  }
278
290
 
291
+ /**
292
+ * The code a host answers, as `TypedResponseError.errorData` (`IAppStoreCatalogUnavailableErrorData`),
293
+ * when it refuses an App Store install, upgrade or upgrade preview because no verified catalog is
294
+ * active. Retryable: the same request succeeds once a catalog is active.
295
+ */
296
+ export const appStoreCatalogUnavailableErrorCode = 'APPSTORE_CATALOG_UNAVAILABLE' as const;
297
+
298
+ export interface IAppStoreCatalogUnavailableErrorData {
299
+ code: typeof appStoreCatalogUnavailableErrorCode;
300
+ retryable: true;
301
+ /** The status `cause` when the host states one. */
302
+ cause?: TAppStoreCatalogUnavailableCause;
303
+ }
304
+
279
305
  /**
280
306
  * The App Store domain's state as an operator sees it, discriminated by `availability`.
281
307
  *
@@ -768,23 +794,167 @@ export const findAppStoreCatalogConfigDigestMismatch = async (
768
794
  return null;
769
795
  };
770
796
 
797
+ /** The registries a consumer loads the catalog from when it configures none. */
798
+ export const appStoreCatalogDefaultRegistries: readonly string[] = Object.freeze(['https://registry.npmjs.org']);
799
+
800
+ const maximumRegistries = 16;
801
+ const maximumRegistryUrlLength = 2048;
802
+
803
+ /** The canonical base URL of one registry entry, or `null` when the entry is not admissible. */
804
+ const readRegistryBaseUrl = (entryArg: unknown): string | null => {
805
+ if (typeof entryArg !== 'string' || entryArg.length > maximumRegistryUrlLength) return null;
806
+ let url: URL;
807
+ try {
808
+ url = new URL(entryArg);
809
+ } catch {
810
+ return null;
811
+ }
812
+ if (url.protocol !== 'https:' || url.username !== '' || url.password !== '' || url.search !== ''
813
+ || url.hash !== '' || entryArg.includes('?') || entryArg.includes('#')) {
814
+ return null;
815
+ }
816
+ return url.toString().replace(/\/+$/, '');
817
+ };
818
+
819
+ /**
820
+ * The errors of a configured registry list; empty when a consumer may load from it. Every entry is
821
+ * an `https:` base URL without credentials, query or fragment, no two name the same registry, and
822
+ * an empty list means `appStoreCatalogDefaultRegistries`. Errors name entries by index and never
823
+ * quote them, so a URL carrying credentials does not reach a log.
824
+ */
825
+ export const validateAppStoreCatalogRegistries = (listArg: unknown): string[] => {
826
+ const entries = readDataElements(listArg, maximumRegistries);
827
+ if (entries === null) return [`registries must be a list of at most ${maximumRegistries} URLs`];
828
+ const errors: string[] = [];
829
+ const seen = new Set<string>();
830
+ entries.forEach((entryArg, indexArg) => {
831
+ const baseUrl = readRegistryBaseUrl(entryArg);
832
+ if (baseUrl === null) {
833
+ errors.push(`registries[${indexArg}] must be an https base URL without credentials, query or fragment`);
834
+ } else if (seen.has(baseUrl)) {
835
+ errors.push(`registries[${indexArg}] names a registry listed before it`);
836
+ } else {
837
+ seen.add(baseUrl);
838
+ }
839
+ });
840
+ return errors;
841
+ };
842
+
843
+ /**
844
+ * The registry base URLs a consumer asks, in order: the configured list in canonical form (no
845
+ * trailing slash), or `appStoreCatalogDefaultRegistries` when it is absent or empty. Throws the
846
+ * `validateAppStoreCatalogRegistries` errors for a list it refuses.
847
+ */
848
+ export const resolveAppStoreCatalogRegistries = (listArg: readonly string[] | undefined): string[] => {
849
+ const errors = validateAppStoreCatalogRegistries(listArg ?? []);
850
+ if (errors.length > 0) throw new Error(`App Store catalog registries refused: ${errors.join('; ')}`);
851
+ const configured = (listArg ?? []).map((entryArg) => readRegistryBaseUrl(entryArg))
852
+ .filter((entryArg): entryArg is string => entryArg !== null);
853
+ return configured.length > 0 ? configured : [...appStoreCatalogDefaultRegistries];
854
+ };
855
+
856
+ /**
857
+ * The App Store version a service runs, as its host took it from a verified catalog.
858
+ *
859
+ * Server-managed: a host writes it when it installs or upgrades the service and when it adopts an
860
+ * existing service on catalog activation, and every runtime path reads this snapshot, never the
861
+ * live catalog. Published versions never change (`judgeAppStoreCatalogSuccession`), so a snapshot
862
+ * stays equal to its catalog entry for as long as the entry is listed.
863
+ */
864
+ export interface IAppStoreServiceTemplateSnapshot {
865
+ appId: string;
866
+ version: string;
867
+ /** The catalog entry's `configDigest`: `computeAppStoreCatalogConfigDigest(config)`. */
868
+ configDigest: TSha256Digest;
869
+ config: TAppStoreCatalogVersionConfig;
870
+ /** The `revision` of the catalog the snapshot was taken from. */
871
+ catalogRevision: number;
872
+ }
873
+
874
+ /**
875
+ * The snapshot of `appId@version` from a verified catalog, or `null` when the catalog does not list
876
+ * it. Withdrawn versions are listed and snapshot like any other: a host adopting an installed
877
+ * service records the version it runs; whether a version is an install or upgrade target is
878
+ * `judgeAppStoreCatalogInstallTarget`. The config is a copy the caller owns.
879
+ */
880
+ export const createAppStoreServiceTemplateSnapshot = (
881
+ catalogArg: IAppStoreCatalog,
882
+ appIdArg: string,
883
+ versionArg: string,
884
+ ): IAppStoreServiceTemplateSnapshot | null => {
885
+ const entry = catalogArg.apps.find((appArg) => appArg.info.id === appIdArg)
886
+ ?.versions.find((candidateArg) => candidateArg.version === versionArg);
887
+ if (entry === undefined) return null;
888
+ return {
889
+ appId: appIdArg,
890
+ version: entry.version,
891
+ configDigest: entry.configDigest,
892
+ config: structuredClone(entry.config),
893
+ catalogRevision: catalogArg.revision,
894
+ };
895
+ };
896
+
897
+ /** The hosts that consume the catalog; each version names its minimum release of each. */
898
+ export type TAppStoreCatalogConsumerKind = 'onebox' | 'cloudly';
899
+
900
+ /** The consumer judging a version: which host it is and the release it runs. */
901
+ export interface IAppStoreCatalogConsumer {
902
+ kind: TAppStoreCatalogConsumerKind;
903
+ /** The host's own release, `MAJOR.MINOR.PATCH`. */
904
+ version: string;
905
+ }
906
+
907
+ const consumerMinimumVersionKeys = Object.freeze({
908
+ onebox: 'minOneboxVersion',
909
+ cloudly: 'minCloudlyVersion',
910
+ } as const);
911
+
771
912
  /** Why a listed version is not an install or upgrade target for this consumer. */
772
913
  export type TAppStoreCatalogInstallTargetRefusal =
773
914
  | { reason: 'versionWithdrawn'; withdrawal: IAppStoreCatalogWithdrawal }
774
- | { reason: 'featuresUnsupported'; missingFeatureIds: string[] };
915
+ | { reason: 'featuresUnsupported'; missingFeatureIds: string[] }
916
+ /** The version names a minimum release of this host (`minOneboxVersion`, `minCloudlyVersion`) newer than it runs. */
917
+ | {
918
+ reason: 'consumerVersionTooOld';
919
+ consumerKind: TAppStoreCatalogConsumerKind;
920
+ consumerVersion: string;
921
+ minimumVersion: string;
922
+ };
775
923
 
776
924
  /**
777
925
  * Whether a consumer may install a listed version, or upgrade a service to it; `null` when it may.
778
- * A withdrawn version and a version requiring a feature the consumer does not fulfil stay listed
779
- * and are refused here, and nowhere else is a second answer needed.
926
+ * A withdrawn version, a version requiring a feature the consumer does not fulfil and a version
927
+ * requiring a newer release of the consumer stay listed and are refused here, in that order, and
928
+ * nowhere else is a second answer needed.
929
+ *
930
+ * A host passes `consumerArg` on every call; without it the minimum release is not judged. A
931
+ * consumer version that is not `MAJOR.MINOR.PATCH` throws: it is the host's own constant.
780
932
  */
781
933
  export const judgeAppStoreCatalogInstallTarget = (
782
934
  versionArg: IAppStoreCatalogVersion,
783
935
  supportedFeatureIdsArg: Iterable<string>,
936
+ consumerArg?: IAppStoreCatalogConsumer,
784
937
  ): TAppStoreCatalogInstallTargetRefusal | null => {
938
+ const consumerVersion = consumerArg === undefined ? null : parseReleaseVersion(consumerArg.version);
939
+ if (consumerArg !== undefined && consumerVersion === null) {
940
+ throw new Error('App Store catalog consumer version must be MAJOR.MINOR.PATCH');
941
+ }
785
942
  if (versionArg.withdrawn !== undefined) return { reason: 'versionWithdrawn', withdrawal: versionArg.withdrawn };
786
943
  const missingFeatureIds = getAppStoreCatalogMissingFeatureIds(versionArg.config, supportedFeatureIdsArg);
787
- return missingFeatureIds.length > 0 ? { reason: 'featuresUnsupported', missingFeatureIds } : null;
944
+ if (missingFeatureIds.length > 0) return { reason: 'featuresUnsupported', missingFeatureIds };
945
+ if (consumerArg === undefined || consumerVersion === null) return null;
946
+ const minimumVersion = versionArg.config[consumerMinimumVersionKeys[consumerArg.kind]];
947
+ if (minimumVersion === undefined) return null;
948
+ const minimum = parseReleaseVersion(minimumVersion);
949
+ if (minimum === null || compareReleaseVersions(consumerVersion, minimum) < 0) {
950
+ return {
951
+ reason: 'consumerVersionTooOld',
952
+ consumerKind: consumerArg.kind,
953
+ consumerVersion: consumerArg.version,
954
+ minimumVersion,
955
+ };
956
+ }
957
+ return null;
788
958
  };
789
959
 
790
960
  export type TAppStoreCatalogSuccession =
@@ -27,6 +27,7 @@ import type {
27
27
  TStorageResourceKind,
28
28
  TStorageSnapshotMode,
29
29
  } from '../platform/storage.js';
30
+ import type { TAppStoreCatalogInstallTargetRefusal } from './catalog.js';
30
31
 
31
32
  export type TAppStorePlatformRequirement =
32
33
  | 'mongodb'
@@ -314,6 +315,18 @@ export const validateAppStoreVersionPlatformOidc = (
314
315
  return errors;
315
316
  };
316
317
 
318
+ /** One listed version of an app as a host shows it: an install target, or why it is not one. */
319
+ export interface IAppStoreAppVersionState {
320
+ version: string;
321
+ /**
322
+ * `null` when the host may install the version or upgrade a service to it; otherwise
323
+ * `versionWithdrawn` with the withdrawal, `featuresUnsupported` with the feature ids the host
324
+ * does not fulfil or `consumerVersionTooOld` with the host release the version requires, as
325
+ * `judgeAppStoreCatalogInstallTarget` answers it.
326
+ */
327
+ installRefusal: TAppStoreCatalogInstallTargetRefusal | null;
328
+ }
329
+
317
330
  export interface IAppStoreApp {
318
331
  id: string;
319
332
  name: string;
@@ -323,6 +336,12 @@ export interface IAppStoreApp {
323
336
  iconUrl?: string;
324
337
  latestVersion: string;
325
338
  versions?: string[];
339
+ /**
340
+ * Every version the catalog lists, withdrawn and feature-gated ones included, in catalog order,
341
+ * each with whether this host may install it. `versions` names only the install targets; a view
342
+ * reads this member to show the others and why they are not offered.
343
+ */
344
+ versionStates?: IAppStoreAppVersionState[];
326
345
  tags?: string[];
327
346
  channel?: string;
328
347
  upgradeStrategy?: TAppStoreUpgradeStrategy;
@@ -346,6 +365,12 @@ export interface IAppStoreAppMeta {
346
365
  iconName?: string;
347
366
  latestVersion: string;
348
367
  versions: string[];
368
+ /**
369
+ * Every version the catalog lists, withdrawn and feature-gated ones included, in catalog order,
370
+ * each with whether this host may install it. `versions` names only the install targets; a view
371
+ * reads this member to show the others and why they are not offered.
372
+ */
373
+ versionStates?: IAppStoreAppVersionState[];
349
374
  maintainer?: string;
350
375
  links?: Record<string, string>;
351
376
  tags?: string[];
@@ -6,7 +6,7 @@ import { readExactDataRecord } from '../private/dataproperties.js';
6
6
  * Cloudly owns the hostname a node should carry (`IClusterNode.data.hostnameIntent`); Spark applies
7
7
  * it on the host and reports the hostname it observes, so a node has converged when its report
8
8
  * states the intent's hostname. The default scheme is `cloudly-worker-<name>`: `name` is the
9
- * operator's short name for the node (`ottilie`, `grasberg`), kept beside the hostname so the node
9
+ * operator's short name for the node (`alpha`, `bravo`), kept beside the hostname so the node
10
10
  * can always be reset to the default however it was renamed.
11
11
  *
12
12
  * A hostname here is one RFC 1123 label, lowercase: the node's short static hostname, which is
@@ -51,8 +51,8 @@ export const deriveDefaultClusterNodeHostname = (nameArg: string): string => {
51
51
  *
52
52
  * It reads the first label of the hostname, so a report that states a dotted name proposes its
53
53
  * short name. A label already in the default scheme proposes what follows the prefix
54
- * (`cloudly-worker-ottilie` → `ottilie`); any other valid label proposes itself unchanged
55
- * (`cloudlyworkergrasberg` stays `cloudlyworkergrasberg`). It never guesses a name out of a
54
+ * (`cloudly-worker-bravo` → `bravo`); any other valid label proposes itself unchanged
55
+ * (`cloudlyworkeralpha` stays `cloudlyworkeralpha`). It never guesses a name out of a
56
56
  * label in another scheme: an operator states that name when resetting the node to the default.
57
57
  */
58
58
  export const proposeClusterNodeName = (hostnameArg: string): string | null => {
@@ -1,6 +1,6 @@
1
1
  import type { IServiceRessources } from './docker.js';
2
2
  import type { IRegistryTarget } from './registry.js';
3
- import type { IAppStorePublishedPort } from '../appstore/index.js';
3
+ import type { IAppStorePublishedPort, IAppStoreServiceTemplateSnapshot } from '../appstore/index.js';
4
4
  import type { IHostedAppLifecycleState } from './hostedapp.js';
5
5
  import type { IServiceMailConfig, TServiceMailWritableConfig } from './mail.js';
6
6
  import type { IServiceWebPushConfig } from './webpush.js';
@@ -175,6 +175,12 @@ export interface IService {
175
175
  appTemplateId?: string;
176
176
  appTemplateVersion?: string;
177
177
  appStoreUpgradePolicy?: 'manual' | 'notify' | 'auto';
178
+ /**
179
+ * Server-managed snapshot of the App Store version the service runs (`appTemplateId`@
180
+ * `appTemplateVersion`), written at install, upgrade and catalog adoption. Runtime paths read
181
+ * it instead of the live catalog.
182
+ */
183
+ appStoreTemplate?: IAppStoreServiceTemplateSnapshot;
178
184
  hostedAppLifecycle?: IHostedAppLifecycleState;
179
185
  reconciliationStatus?: IServiceReconciliationStatus;
180
186
  environment: { [key: string]: string };
@@ -254,11 +260,13 @@ export interface IService {
254
260
 
255
261
  /**
256
262
  * Caller-writable service data. Immutable rollout state and SecretSet
257
- * attachments are changed only through their dedicated fenced APIs.
263
+ * attachments are changed only through their dedicated fenced APIs, and the
264
+ * App Store template snapshot only by the host's install and upgrade.
258
265
  */
259
266
  export type TServiceWritableData = Omit<
260
267
  IService['data'],
261
268
  | 'organizationId'
269
+ | 'appStoreTemplate'
262
270
  | 'immutableImageRequired'
263
271
  | 'imageDeployment'
264
272
  | 'imageRolloutStatus'
@@ -63,6 +63,12 @@ export interface ICloudlySettings {
63
63
  coreMailDefaultMaxPendingInbound?: number;
64
64
  corebuildWorkerUrl?: string;
65
65
  corebuildWorkerUrls?: string[];
66
+ /**
67
+ * The npm registries Cloudly loads the App Store catalog from, in order. Absent or empty means
68
+ * `appstore.appStoreCatalogDefaultRegistries`; a value is accepted only when
69
+ * `appstore.validateAppStoreCatalogRegistries` answers no error.
70
+ */
71
+ appStoreCatalogRegistries?: string[];
66
72
  }
67
73
 
68
74
  export const cloudlySystemSecretOwner = Object.freeze({
@@ -112,6 +112,28 @@ export interface IReq_Any_RefreshAppStoreCatalog extends plugins.typedrequestInt
112
112
  };
113
113
  }
114
114
 
115
+ /**
116
+ * Imports a signed catalog envelope an operator carried in (an air-gapped host, or a catalog the
117
+ * registries do not serve yet) and answers the state after it. The host judges it exactly as a
118
+ * fetched one, authentication and succession included, and records it with origin
119
+ * `operatorImport`; the outcome is `status.lastAttempt`, and a refused envelope keeps the active
120
+ * catalog in place and is not a request error.
121
+ */
122
+ export interface IReq_Any_ImportAppStoreCatalog extends plugins.typedrequestInterfaces.implementsTR<
123
+ plugins.typedrequestInterfaces.ITypedRequest,
124
+ IReq_Any_ImportAppStoreCatalog
125
+ > {
126
+ method: 'importAppStoreCatalog';
127
+ request: {
128
+ identity: IIdentity;
129
+ /** The envelope file's text (`IAppStoreCatalogEnvelope` as JSON), e.g. `catalog.envelope.json`. */
130
+ envelopeText: string;
131
+ };
132
+ response: {
133
+ status: TAppStoreCatalogStatus;
134
+ };
135
+ }
136
+
115
137
  export interface IReq_Any_GetAppStoreConfig extends plugins.typedrequestInterfaces.implementsTR<
116
138
  plugins.typedrequestInterfaces.ITypedRequest,
117
139
  IReq_Any_GetAppStoreConfig
@@ -32,6 +32,12 @@ export interface IReq_GetHostedAppAccessConfiguration extends plugins.typedreque
32
32
  roles: data.IHostedAppRoleDefinition[];
33
33
  platformOidcSupported: boolean;
34
34
  platformOidcRegistration?: data.IHostedAppPlatformOidcRegistration;
35
+ /**
36
+ * Present when the host cannot read this app's roles and platform OIDC capability, e.g. the
37
+ * service holds no App Store template snapshot yet; `roles` is then empty and
38
+ * `platformOidcSupported` false. One such app never fails the whole listing.
39
+ */
40
+ unavailableReason?: string;
35
41
  }>;
36
42
  };
37
43
  }
@@ -369,7 +369,7 @@ export interface IReq_Any_Cloudly_SetClusterNodeCorestoreDatabaseEndpoint extend
369
369
  *
370
370
  * The node's name stays as it is: the new intent keeps the name of the node's current intent. A
371
371
  * node without an intent takes the name `data.proposeClusterNodeName(hostname)` (`foo` names the
372
- * node `foo`, `cloudly-worker-grasberg` names it `grasberg`), and is refused `name-invalid` when
372
+ * node `foo`, `cloudly-worker-alpha` names it `alpha`), and is refused `name-invalid` when
373
373
  * that is `null`.
374
374
  *
375
375
  * Cloudly records the intent and answers at once; Spark applies the intent of the latest
@@ -262,6 +262,11 @@ extends plugins.typedrequestInterfaces.implementsTR<
262
262
  };
263
263
  }
264
264
 
265
+ /**
266
+ * Answers one deployment operation's status and advances it: a rolling-out operation whose runtime
267
+ * evidence proves success or failure is transitioned before the answer. A deployment that polls
268
+ * its own rollout uses it; an inspection uses `readServiceDeploymentStatus`.
269
+ */
265
270
  export interface IRequest_Any_Cloudly_GetServiceDeploymentStatus
266
271
  extends plugins.typedrequestInterfaces.implementsTR<
267
272
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -278,6 +283,29 @@ extends plugins.typedrequestInterfaces.implementsTR<
278
283
  };
279
284
  }
280
285
 
286
+ /**
287
+ * Answers one deployment operation's status, as `getServiceDeploymentStatus` projects it, and
288
+ * changes nothing: no transition, no failure recorded, no recovery scheduled. A rollout failure or
289
+ * an expired startup grace shows in the returned evidence only, and the persisted operation keeps
290
+ * its state, revision, timestamps and evidence. It takes the same identity and the
291
+ * `deployment:status` capability.
292
+ */
293
+ export interface IRequest_Any_Cloudly_ReadServiceDeploymentStatus
294
+ extends plugins.typedrequestInterfaces.implementsTR<
295
+ plugins.typedrequestInterfaces.ITypedRequest,
296
+ IRequest_Any_Cloudly_ReadServiceDeploymentStatus
297
+ > {
298
+ method: 'readServiceDeploymentStatus';
299
+ request: {
300
+ identity: IIdentityCredential;
301
+ serviceId: string;
302
+ operationId: string;
303
+ };
304
+ response: {
305
+ status: IServiceDeploymentStatus;
306
+ };
307
+ }
308
+
281
309
  export interface IRequest_Any_Cloudly_PromoteServiceDeploymentRoute
282
310
  extends plugins.typedrequestInterfaces.implementsTR<
283
311
  plugins.typedrequestInterfaces.ITypedRequest,