@serve.zone/interfaces 32.48.0 → 32.50.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 (39) hide show
  1. package/changelog.md +67 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/appstore/catalog.d.ts +300 -0
  4. package/dist_ts/appstore/catalog.js +544 -0
  5. package/dist_ts/appstore/index.d.ts +2 -0
  6. package/dist_ts/appstore/index.js +3 -1
  7. package/dist_ts/appstore/types.d.ts +9 -0
  8. package/dist_ts/appstore/types.js +1 -1
  9. package/dist_ts/appstore/versionconfig.d.ts +21 -0
  10. package/dist_ts/appstore/versionconfig.js +685 -0
  11. package/dist_ts/data/index.d.ts +1 -0
  12. package/dist_ts/data/index.js +2 -1
  13. package/dist_ts/data/networkapplication.d.ts +84 -0
  14. package/dist_ts/data/networkapplication.js +128 -0
  15. package/dist_ts/data/networkprojection.d.ts +5 -3
  16. package/dist_ts/data/networkprojection.js +6 -4
  17. package/dist_ts/private/keylist.d.ts +5 -0
  18. package/dist_ts/private/keylist.js +2 -0
  19. package/dist_ts/requests/appstore.d.ts +30 -1
  20. package/dist_ts/requests/runtimesession.d.ts +11 -0
  21. package/dist_ts/runtime.appstorecatalog.d.ts +46 -0
  22. package/dist_ts/runtime.appstorecatalog.js +142 -0
  23. package/dist_ts/runtime.d.ts +1 -0
  24. package/dist_ts/runtime.js +2 -1
  25. package/package.json +1 -1
  26. package/readme.md +127 -4
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/appstore/catalog.ts +851 -0
  29. package/ts/appstore/index.ts +2 -0
  30. package/ts/appstore/types.ts +9 -0
  31. package/ts/appstore/versionconfig.ts +803 -0
  32. package/ts/data/index.ts +1 -0
  33. package/ts/data/networkapplication.ts +188 -0
  34. package/ts/data/networkprojection.ts +5 -3
  35. package/ts/private/keylist.ts +6 -0
  36. package/ts/requests/appstore.ts +38 -0
  37. package/ts/requests/runtimesession.ts +14 -0
  38. package/ts/runtime.appstorecatalog.ts +195 -0
  39. package/ts/runtime.ts +1 -0
package/ts/data/index.ts CHANGED
@@ -36,6 +36,7 @@ export * from './nodeenrollment.js';
36
36
  export * from './noderuntimebinding.js';
37
37
  export * from './noderetirement.js';
38
38
  export * from './networkaddressplan.js';
39
+ export * from './networkapplication.js';
39
40
  export * from './networkauthority.js';
40
41
  export * from './networkcredential.js';
41
42
  export * from './networkdelivery.js';
@@ -0,0 +1,188 @@
1
+ import type { TSha256Digest } from './immutableimage.js';
2
+ import type { IRuntimeAuthorityReference } from './runtimeassignment.js';
3
+ import type { IRuntimeNetworkNodeScope } from './networkprojection.js';
4
+ import { computeRuntimeNetworkProjectionDigest, snapshotRuntimeNetworkNodeScope,
5
+ snapshotRuntimeNetworkProjection } from './networkprojection.js';
6
+ import type { IRuntimeSessionBinding } from './runtimesession.js';
7
+ import { snapshotRuntimeSessionBinding } from './runtimesession.js';
8
+ import { captureNetwork, failNetwork, networkDigest, networkEqual, networkHash, networkId, networkRecord,
9
+ networkReference, networkReferenceOf, networkRevision, networkSafe, requireNetwork } from '../private/networkcontract.js';
10
+
11
+ /**
12
+ * A node's durable statement that its packet authority is bounded by one admitted projection.
13
+ *
14
+ * Every packet table the node holds was composed from `projection`, or the node holds none: the
15
+ * flows its kernel admits are a subset of that projection's grants. Only the node's joined native
16
+ * packet owner can supply it — the journaled application of the projection's policies on every
17
+ * role, or the release of every table — never an admission ACK, a single table inspection or a
18
+ * caller-selected flag. A crash, a reboot or a restored guard only removes authority, so a receipt
19
+ * never becomes false; applying a successor projection supersedes it.
20
+ *
21
+ * `nativeJournal` names the node-private joined packet journal that applied the projection, or is
22
+ * null when the node holds no packet table at all. The receipts of one node form one monotonic
23
+ * chain; a receipt never names an older projection than its predecessor. These portable values
24
+ * neither inspect a kernel nor authenticate their producer: the receiver binds the report to the
25
+ * current authenticated session or its own reporter history.
26
+ */
27
+ export interface IRuntimeNetworkProjectionApplicationReceipt extends IRuntimeNetworkNodeScope {
28
+ id: string;
29
+ generation: number;
30
+ previous: IRuntimeAuthorityReference | null;
31
+ projection: IRuntimeAuthorityReference;
32
+ reporterSessionId: string;
33
+ bootId: string;
34
+ nativeJournal: IRuntimeAuthorityReference | null;
35
+ digest: TSha256Digest;
36
+ }
37
+
38
+ export interface IReportRuntimeNetworkProjectionApplicationRequest {
39
+ session: IRuntimeSessionBinding;
40
+ receipt: IRuntimeNetworkProjectionApplicationReceipt;
41
+ }
42
+ export interface IReportRuntimeNetworkProjectionApplicationResponse {
43
+ status: 'accepted' | 'replay';
44
+ receipt: IRuntimeAuthorityReference;
45
+ authorityUse: 'current' | 'historical';
46
+ }
47
+
48
+ export const runtimeNetworkProjectionApplicationContract = Object.freeze({
49
+ digestDomain: 'serve.zone/runtime-network-projection-application-receipt',
50
+ claim: 'packet-authority-within-the-grants-of-the-named-projection',
51
+ receiptSource: 'joined-native-packet-journal-or-no-packet-table',
52
+ appliedPrevious: 'only-a-receipt-naming-the-exact-previous-projection',
53
+ olderProjection: 'advances-the-chain-never-applied-previous',
54
+ acknowledgement: 'an-admission-ack-is-never-an-application-receipt',
55
+ authentication: 'current-authenticated-session-or-receiver-reporter-history',
56
+ } as const);
57
+
58
+ const bootIdPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
59
+ const scopeOf = (value: IRuntimeNetworkNodeScope): IRuntimeNetworkNodeScope =>
60
+ ({ controller: value.controller, nodeId: value.nodeId, runtimeNamespace: value.runtimeNamespace });
61
+ const bootId = (value: unknown): string => {
62
+ requireNetwork(typeof value === 'string' && bootIdPattern.test(value) && value !== '00000000-0000-0000-0000-000000000000');
63
+ return value;
64
+ };
65
+
66
+ /** Exact-key structural snapshot; digest and chain rules are `admitRuntimeNetworkProjectionApplicationReceipt`'s. */
67
+ export const snapshotRuntimeNetworkProjectionApplicationReceipt = (
68
+ value: unknown,
69
+ ): IRuntimeNetworkProjectionApplicationReceipt => networkSafe(() => {
70
+ const item = networkRecord(captureNetwork(value), ['id', 'generation', 'previous', 'controller', 'nodeId',
71
+ 'runtimeNamespace', 'projection', 'reporterSessionId', 'bootId', 'nativeJournal', 'digest']);
72
+ const scope = snapshotRuntimeNetworkNodeScope({ controller: item.controller, nodeId: item.nodeId,
73
+ runtimeNamespace: item.runtimeNamespace });
74
+ return { ...networkRevision(item), ...scope, projection: networkReference(item.projection),
75
+ reporterSessionId: networkId(item.reporterSessionId), bootId: bootId(item.bootId),
76
+ nativeJournal: item.nativeJournal === null ? null : networkReference(item.nativeJournal),
77
+ digest: networkDigest(item.digest) };
78
+ });
79
+
80
+ export const computeRuntimeNetworkProjectionApplicationReceiptDigest = (
81
+ value: IRuntimeNetworkProjectionApplicationReceipt,
82
+ ): Promise<TSha256Digest> => networkHash(runtimeNetworkProjectionApplicationContract.digestDomain,
83
+ snapshotRuntimeNetworkProjectionApplicationReceipt(value));
84
+
85
+ /**
86
+ * Pure chain binding of one reported receipt. `previousReceiptValue` is the receiver's current
87
+ * receipt for the node, or null while it holds none; `headValue` is the receiver's current
88
+ * projection for the node. The owner must authenticate the reporter and fence its writes.
89
+ *
90
+ * The receipt names the head's projection chain at a generation the head has reached: the head
91
+ * itself, exactly, or an older generation. An older one is a receipt minted before the node
92
+ * admitted the head; it advances the chain and is never usable as `appliedPrevious`.
93
+ */
94
+ export const admitRuntimeNetworkProjectionApplicationReceipt = async (
95
+ value: unknown, previousReceiptValue: unknown | null, trustedScopeValue: unknown, headValue: unknown,
96
+ ): Promise<{ status: 'accepted' | 'replay'; receipt: IRuntimeNetworkProjectionApplicationReceipt }> => {
97
+ try {
98
+ const receipt = snapshotRuntimeNetworkProjectionApplicationReceipt(value);
99
+ const previous = previousReceiptValue === null ? null : snapshotRuntimeNetworkProjectionApplicationReceipt(previousReceiptValue);
100
+ const scope = snapshotRuntimeNetworkNodeScope(captureNetwork(trustedScopeValue));
101
+ const head = snapshotRuntimeNetworkProjection(headValue);
102
+ requireNetwork(networkEqual(scopeOf(receipt), scope) && networkEqual(scopeOf(head), scope));
103
+ requireNetwork(receipt.digest === await computeRuntimeNetworkProjectionApplicationReceiptDigest(receipt)
104
+ && head.digest === await computeRuntimeNetworkProjectionDigest(head));
105
+ requireNetwork(receipt.projection.id === head.id && receipt.projection.generation <= head.generation);
106
+ if (receipt.projection.generation === head.generation) requireNetwork(networkEqual(receipt.projection, networkReferenceOf(head)));
107
+ if (previous === null) requireNetwork(receipt.generation === 1 && receipt.previous === null);
108
+ else {
109
+ requireNetwork(networkEqual(scopeOf(previous), scope)
110
+ && previous.digest === await computeRuntimeNetworkProjectionApplicationReceiptDigest(previous));
111
+ if (networkEqual(receipt, previous)) return { status: 'replay', receipt };
112
+ requireNetwork(receipt.id === previous.id && receipt.generation === previous.generation + 1
113
+ && networkEqual(receipt.previous, networkReferenceOf(previous))
114
+ && receipt.projection.id === previous.projection.id
115
+ && receipt.projection.generation >= previous.projection.generation);
116
+ if (receipt.projection.generation === previous.projection.generation) {
117
+ requireNetwork(networkEqual(receipt.projection, previous.projection));
118
+ }
119
+ }
120
+ return { status: 'accepted', receipt };
121
+ } catch { return failNetwork(); }
122
+ };
123
+
124
+ /**
125
+ * The `appliedPrevious` argument of `admitRuntimeNetworkProjection` for the successor of
126
+ * `previousProjectionValue`: the receipt's projection when it is exactly that projection, else
127
+ * null. `receiptValue` must be the receiver's own admitted receipt for the node (a node passes its
128
+ * own durable receipt, a controller the latest receipt it admitted), never an ACK field.
129
+ */
130
+ export const selectRuntimeNetworkProjectionAppliedPrevious = async (
131
+ receiptValue: unknown | null, previousProjectionValue: unknown,
132
+ ): Promise<IRuntimeAuthorityReference | null> => {
133
+ try {
134
+ // Snapshot both caller-owned inputs before the first asynchronous operation.
135
+ const previous = snapshotRuntimeNetworkProjection(previousProjectionValue);
136
+ const receipt = receiptValue === null ? null : snapshotRuntimeNetworkProjectionApplicationReceipt(receiptValue);
137
+ requireNetwork(previous.digest === await computeRuntimeNetworkProjectionDigest(previous));
138
+ if (receipt === null) return null;
139
+ requireNetwork(networkEqual(scopeOf(receipt), scopeOf(previous))
140
+ && receipt.digest === await computeRuntimeNetworkProjectionApplicationReceiptDigest(receipt));
141
+ return networkEqual(receipt.projection, networkReferenceOf(previous)) ? networkReferenceOf(receipt.projection) : null;
142
+ } catch { return failNetwork(); }
143
+ };
144
+
145
+ export const snapshotReportRuntimeNetworkProjectionApplicationRequest = (
146
+ value: unknown,
147
+ ): IReportRuntimeNetworkProjectionApplicationRequest => networkSafe(() => {
148
+ const item = networkRecord(captureNetwork(value), ['session', 'receipt']);
149
+ return { session: snapshotRuntimeSessionBinding(item.session),
150
+ receipt: snapshotRuntimeNetworkProjectionApplicationReceipt(item.receipt) };
151
+ });
152
+
153
+ /**
154
+ * Both trusted bindings come from the receiver's fenced physical-session/history owner: the
155
+ * current session, and the session history entry that names the receipt's reporter. A receipt
156
+ * minted under an earlier session of the same node still drains from its outbox as `historical`.
157
+ */
158
+ export const bindReportRuntimeNetworkProjectionApplicationRequest = (
159
+ value: unknown, trustedCurrentSessionValue: unknown, trustedReporterSessionValue: unknown,
160
+ ): { request: IReportRuntimeNetworkProjectionApplicationRequest; authorityUse: 'current' | 'historical' } => networkSafe(() => {
161
+ const request = snapshotReportRuntimeNetworkProjectionApplicationRequest(value);
162
+ const current = snapshotRuntimeSessionBinding(captureNetwork(trustedCurrentSessionValue));
163
+ const reporter = snapshotRuntimeSessionBinding(captureNetwork(trustedReporterSessionValue));
164
+ requireNetwork(networkEqual(request.session, current) && networkEqual(scopeOf(request.receipt), scopeOf(current))
165
+ && networkEqual(scopeOf(reporter), scopeOf(current)) && reporter.reporterSessionId === request.receipt.reporterSessionId);
166
+ const authorityUse = reporter.reporterSessionId === current.reporterSessionId ? 'current' : 'historical';
167
+ requireNetwork(authorityUse === 'current' ? networkEqual(reporter, current)
168
+ : reporter.sessionEpoch < current.sessionEpoch && reporter.credentialGeneration <= current.credentialGeneration);
169
+ return { request, authorityUse };
170
+ });
171
+
172
+ export const snapshotReportRuntimeNetworkProjectionApplicationResponse = (
173
+ value: unknown,
174
+ ): IReportRuntimeNetworkProjectionApplicationResponse => networkSafe(() => {
175
+ const item = networkRecord(captureNetwork(value), ['status', 'receipt', 'authorityUse']);
176
+ requireNetwork((item.status === 'accepted' || item.status === 'replay')
177
+ && (item.authorityUse === 'current' || item.authorityUse === 'historical'));
178
+ return { status: item.status, receipt: networkReference(item.receipt), authorityUse: item.authorityUse };
179
+ });
180
+
181
+ /** Compare the response with the exact SENT receipt, never with a response-derived value. */
182
+ export const bindReportRuntimeNetworkProjectionApplicationResponse = (
183
+ value: unknown, sentReceiptValue: unknown,
184
+ ): IReportRuntimeNetworkProjectionApplicationResponse => networkSafe(() => {
185
+ const response = snapshotReportRuntimeNetworkProjectionApplicationResponse(value);
186
+ requireNetwork(networkEqual(response.receipt, networkReference(captureNetwork(sentReceiptValue))));
187
+ return response;
188
+ });
@@ -788,9 +788,11 @@ export const bindRuntimeNetworkWorkloadLeaseToAssignment = async (leaseValue: un
788
788
  };
789
789
 
790
790
  /**
791
- * Pure revision decision. appliedPrevious must be the receiver's exact durable,
792
- * joined application receipt, never an incoming ACK field. All prior accepted
793
- * material/apply journals survive until effects and DNS/allocation quarantine end.
791
+ * Pure revision decision. appliedPrevious must come from the receiver's exact durable,
792
+ * joined application receipt (`IRuntimeNetworkProjectionApplicationReceipt`, selected by
793
+ * `selectRuntimeNetworkProjectionAppliedPrevious`), never from an incoming ACK field. Only a
794
+ * receipt naming exactly `previousValue` releases its carried withdrawals and tombstones. All
795
+ * prior accepted material/apply journals survive until effects and DNS/allocation quarantine end.
794
796
  */
795
797
  export const admitRuntimeNetworkProjection = async (
796
798
  value: unknown, previousValue: unknown | null, trustedScopeValue: unknown, appliedPreviousValue: unknown | null,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A key list that names every key of a record type, so a member added to the type cannot be
3
+ * forgotten by a validator built on the list: an incomplete list does not type-check.
4
+ */
5
+ export type TCompleteKeyList<TRecord, TKeys extends readonly (keyof TRecord)[]> =
6
+ Exclude<keyof TRecord, TKeys[number]> extends never ? TKeys : never;
@@ -4,6 +4,7 @@ import type { IService } from '../data/service.js';
4
4
  import type {
5
5
  IAppStoreApp,
6
6
  IAppStoreAppMeta,
7
+ TAppStoreCatalogStatus,
7
8
  IAppStoreInstallRequest,
8
9
  IAppStoreVersionConfig,
9
10
  IUpgradeableAppStoreService,
@@ -71,6 +72,43 @@ export interface IReq_Any_GetAppStoreTemplates extends plugins.typedrequestInter
71
72
  };
72
73
  response: {
73
74
  apps: IAppStoreApp[];
75
+ /**
76
+ * The catalog state behind `apps`. Optional while consumers still serve a bundled catalog;
77
+ * a consumer that loads the catalog at runtime always answers it.
78
+ */
79
+ catalogStatus?: TAppStoreCatalogStatus;
80
+ };
81
+ }
82
+
83
+ /** The App Store catalog's state: availability, the active catalog, the last attempt and refusal. */
84
+ export interface IReq_Any_GetAppStoreCatalogStatus extends plugins.typedrequestInterfaces.implementsTR<
85
+ plugins.typedrequestInterfaces.ITypedRequest,
86
+ IReq_Any_GetAppStoreCatalogStatus
87
+ > {
88
+ method: 'getAppStoreCatalogStatus';
89
+ request: {
90
+ identity: IIdentity;
91
+ };
92
+ response: {
93
+ status: TAppStoreCatalogStatus;
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Runs one bounded catalog load attempt now, outside the refresh schedule, and answers the state
99
+ * after it. The attempt's outcome is `status.lastAttempt`; a refused or failed attempt keeps the
100
+ * active catalog in place and is not a request error.
101
+ */
102
+ export interface IReq_Any_RefreshAppStoreCatalog extends plugins.typedrequestInterfaces.implementsTR<
103
+ plugins.typedrequestInterfaces.ITypedRequest,
104
+ IReq_Any_RefreshAppStoreCatalog
105
+ > {
106
+ method: 'refreshAppStoreCatalog';
107
+ request: {
108
+ identity: IIdentity;
109
+ };
110
+ response: {
111
+ status: TAppStoreCatalogStatus;
74
112
  };
75
113
  }
76
114
 
@@ -4,6 +4,7 @@ import type * as binding from '../data/noderuntimebinding.js';
4
4
  import type * as terminal from '../data/runtimeterminalreceipt.js';
5
5
  import type * as network from '../data/networkdelivery.js';
6
6
  import type * as protection from '../data/networkprotection.js';
7
+ import type * as application from '../data/networkapplication.js';
7
8
  import type * as credential from '../data/networkcredential.js';
8
9
  import type * as assignmentSecret from '../runtime.assignmentsecret.js';
9
10
  import type * as fleet from '../data/fleetcutover.js';
@@ -32,6 +33,19 @@ export interface IReq_Pallet_Controller_ReportRuntimeNetworkProtection extends p
32
33
  response: protection.IReportRuntimeNetworkProtectionResponse;
33
34
  }
34
35
 
36
+ /**
37
+ * Current authenticated peer reports its durable joined application receipt: the projection that
38
+ * bounds its packet authority. The controller passes it as `appliedPrevious` only while it names
39
+ * the exact head; an admission ACK never stands in for it.
40
+ */
41
+ export interface IReq_Pallet_Controller_ReportRuntimeNetworkProjectionApplication extends plugins.typedrequestInterfaces.implementsTR<
42
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Pallet_Controller_ReportRuntimeNetworkProjectionApplication
43
+ > {
44
+ method: 'reportRuntimeNetworkProjectionApplication';
45
+ request: application.IReportRuntimeNetworkProjectionApplicationRequest;
46
+ response: application.IReportRuntimeNetworkProjectionApplicationResponse;
47
+ }
48
+
35
49
  /**
36
50
  * Current authenticated Pallet session only. Cloudly reads the current pallet runtime and exact
37
51
  * owner records, then grants one held prepared handoff; serialized session content is no proof.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Ed25519 signing and verification of App Store catalog envelopes (Node-only).
3
+ *
4
+ * A consumer authenticates before it interprets: the envelope's digest and signature are checked
5
+ * before the catalog's contract and schema are read.
6
+ */
7
+ import * as plugins from './plugins.runtime.js';
8
+ import type { TSha256Digest } from './data/immutableimage.js';
9
+ import { isSha256Digest } from './data/immutableimage.js';
10
+ import { canonicalizeStrictJson } from './private/canonicaljson.js';
11
+ import { readDataElements, readDataProperties } from './private/dataproperties.js';
12
+ import {
13
+ appStoreCatalogContract,
14
+ computeAppStoreCatalogDigest,
15
+ computeAppStoreCatalogSigningKeyId,
16
+ createAppStoreCatalogSignaturePayload,
17
+ findAppStoreCatalogConfigDigestMismatch,
18
+ isAppStoreCatalogEnvelope,
19
+ judgeAppStoreCatalogContract,
20
+ validateAppStoreCatalog,
21
+ validateAppStoreCatalogEnvelope,
22
+ validateAppStoreCatalogEnvelopeShape,
23
+ } from './appstore/catalog.js';
24
+ import type {
25
+ IAppStoreCatalog,
26
+ IAppStoreCatalogEnvelope,
27
+ IAppStoreCatalogRefusal,
28
+ IAppStoreCatalogSignature,
29
+ IAppStoreCatalogTrustedKey,
30
+ IAppStoreCatalogValidationOptions,
31
+ TAppStoreCatalogRefusalReason,
32
+ } from './appstore/catalog.js';
33
+
34
+ /** A catalog signing key. The private half is process-local; keep it in the publisher's secret store. */
35
+ export interface IAppStoreCatalogSigningKey {
36
+ keyId: TSha256Digest;
37
+ /** Raw 32-byte Ed25519 public key, lower-case hex. */
38
+ publicKey: string;
39
+ privateKey: plugins.crypto.KeyObject;
40
+ }
41
+
42
+ export interface IAppStoreCatalogVerificationOptions extends IAppStoreCatalogValidationOptions {
43
+ /** The keys this consumer trusts, shipped with its release. */
44
+ trustedKeys: readonly IAppStoreCatalogTrustedKey[];
45
+ }
46
+
47
+ export type TAppStoreCatalogVerification =
48
+ | { accepted: true; envelope: IAppStoreCatalogEnvelope; keyId: TSha256Digest }
49
+ | { accepted: false; refusal: IAppStoreCatalogRefusal };
50
+
51
+ const rawPublicKeyOf = (keyArg: plugins.crypto.KeyObject): string => {
52
+ const jwk = keyArg.export({ format: 'jwk' });
53
+ if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string') {
54
+ throw new Error('App Store catalog signing key must be an Ed25519 key');
55
+ }
56
+ return Buffer.from(jwk.x, 'base64url').toString('hex');
57
+ };
58
+
59
+ const publicKeyObjectOf = (publicKeyArg: string): plugins.crypto.KeyObject =>
60
+ plugins.crypto.createPublicKey({
61
+ format: 'jwk',
62
+ key: { kty: 'OKP', crv: 'Ed25519', x: Buffer.from(publicKeyArg, 'hex').toString('base64url') },
63
+ });
64
+
65
+ /** A fresh Ed25519 catalog signing key with its key id. */
66
+ export const createAppStoreCatalogSigningKey = async (): Promise<IAppStoreCatalogSigningKey> => {
67
+ const pair = plugins.crypto.generateKeyPairSync('ed25519');
68
+ const publicKey = rawPublicKeyOf(pair.publicKey);
69
+ return { keyId: await computeAppStoreCatalogSigningKeyId(publicKey), publicKey, privateKey: pair.privateKey };
70
+ };
71
+
72
+ /**
73
+ * Signs only a catalog this contract accepts, whose every `configDigest` matches its config.
74
+ *
75
+ * `optionsArg.supportedFeatureIds` should name every feature id the catalog uses, so the
76
+ * publisher judges every version config in full.
77
+ */
78
+ export const signAppStoreCatalog = async (
79
+ catalogArg: IAppStoreCatalog,
80
+ signingKeysArg: readonly IAppStoreCatalogSigningKey[],
81
+ optionsArg: IAppStoreCatalogValidationOptions,
82
+ ): Promise<IAppStoreCatalogEnvelope> => {
83
+ const errors = validateAppStoreCatalog(catalogArg, optionsArg);
84
+ if (errors.length > 0) {
85
+ throw new Error(`App Store catalog refused for signing: ${errors.join('; ')}`);
86
+ }
87
+ const mismatch = await findAppStoreCatalogConfigDigestMismatch(catalogArg);
88
+ if (mismatch !== null) throw new Error(`App Store catalog refused for signing: ${mismatch}`);
89
+ if (signingKeysArg.length === 0) throw new Error('App Store catalog needs at least one signing key');
90
+ const catalogDigest = await computeAppStoreCatalogDigest(catalogArg);
91
+ const payload = createAppStoreCatalogSignaturePayload(catalogDigest);
92
+ const signatures: IAppStoreCatalogSignature[] = [];
93
+ for (const key of signingKeysArg) {
94
+ const { privateKey } = key;
95
+ if (!(privateKey instanceof plugins.crypto.KeyObject) || privateKey.type !== 'private'
96
+ || privateKey.asymmetricKeyType !== 'ed25519') {
97
+ throw new Error('App Store catalog signing key must be an Ed25519 private key');
98
+ }
99
+ const publicKey = rawPublicKeyOf(plugins.crypto.createPublicKey(privateKey));
100
+ if (publicKey !== key.publicKey || await computeAppStoreCatalogSigningKeyId(publicKey) !== key.keyId) {
101
+ throw new Error(`App Store catalog signing key ${key.keyId} does not match its private key`);
102
+ }
103
+ signatures.push({
104
+ keyId: key.keyId,
105
+ signature: plugins.crypto.sign(null, payload, privateKey).toString('hex'),
106
+ });
107
+ }
108
+ return { catalog: structuredClone(catalogArg), catalogDigest, signatures };
109
+ };
110
+
111
+ const refuse = (reasonArg: TAppStoreCatalogRefusalReason, messageArg: string): TAppStoreCatalogVerification =>
112
+ ({ accepted: false, refusal: { reason: reasonArg, message: messageArg } });
113
+
114
+ const failCanonicalInput = (reasonArg: string): never => {
115
+ throw new Error(reasonArg);
116
+ };
117
+
118
+ /**
119
+ * Judges an envelope a consumer fetched or an operator imported.
120
+ *
121
+ * In order: exact envelope shape, `catalogDigest` against the catalog, one valid signature by a
122
+ * trusted key (and no failing one), the contract identity, the catalog schema, every
123
+ * `configDigest`. The accepted envelope is an inert copy of the input. Succession against the
124
+ * active catalog is the caller's next step (`judgeAppStoreCatalogSuccession`).
125
+ */
126
+ export const verifyAppStoreCatalogEnvelope = async (
127
+ envelopeArg: unknown,
128
+ optionsArg: IAppStoreCatalogVerificationOptions,
129
+ ): Promise<TAppStoreCatalogVerification> => {
130
+ let inert: unknown;
131
+ try {
132
+ inert = JSON.parse(canonicalizeStrictJson(envelopeArg, failCanonicalInput, 'envelope'));
133
+ } catch (error) {
134
+ return refuse('schemaInvalid', error instanceof Error ? error.message : 'envelope is not canonical JSON');
135
+ }
136
+ const shapeErrors = validateAppStoreCatalogEnvelopeShape(inert);
137
+ const envelope = readDataProperties(inert);
138
+ const signatures = envelope === null
139
+ ? null
140
+ : readDataElements(envelope.signatures, appStoreCatalogContract.maximumSignatures);
141
+ if (shapeErrors.length > 0 || envelope === null || signatures === null
142
+ || typeof envelope.catalogDigest !== 'string' || !isSha256Digest(envelope.catalogDigest)) {
143
+ return refuse('schemaInvalid', shapeErrors.join('; ') || 'envelope must be an object');
144
+ }
145
+
146
+ if (await computeAppStoreCatalogDigest(envelope.catalog) !== envelope.catalogDigest) {
147
+ return refuse('digestMismatch', 'catalogDigest does not match the catalog');
148
+ }
149
+ const trustedKeys = new Map<string, string>();
150
+ for (const trustedKey of optionsArg.trustedKeys) {
151
+ let keyId: TSha256Digest;
152
+ try {
153
+ keyId = await computeAppStoreCatalogSigningKeyId(trustedKey.publicKey);
154
+ } catch {
155
+ return refuse('signatureInvalid', `trusted key ${trustedKey.keyId} is not a raw Ed25519 public key`);
156
+ }
157
+ if (keyId !== trustedKey.keyId) {
158
+ return refuse('signatureInvalid', `trusted key ${trustedKey.keyId} does not match its public key`);
159
+ }
160
+ trustedKeys.set(keyId, trustedKey.publicKey);
161
+ }
162
+ const payload = createAppStoreCatalogSignaturePayload(envelope.catalogDigest);
163
+ let verifiedKeyId: TSha256Digest | null = null;
164
+ for (const signatureArg of signatures) {
165
+ const signature = readDataProperties(signatureArg);
166
+ if (signature === null || typeof signature.keyId !== 'string' || !isSha256Digest(signature.keyId)
167
+ || typeof signature.signature !== 'string') {
168
+ return refuse('schemaInvalid', 'envelope.signatures entries must be objects');
169
+ }
170
+ const publicKey = trustedKeys.get(signature.keyId);
171
+ if (publicKey === undefined) continue;
172
+ const valid = plugins.crypto.verify(
173
+ null,
174
+ payload,
175
+ publicKeyObjectOf(publicKey),
176
+ Buffer.from(signature.signature, 'hex'),
177
+ );
178
+ if (!valid) {
179
+ return refuse('signatureInvalid', `signature by trusted key ${signature.keyId} does not verify`);
180
+ }
181
+ verifiedKeyId = signature.keyId;
182
+ }
183
+ if (verifiedKeyId === null) {
184
+ return refuse('signatureInvalid', 'no signature by a trusted key');
185
+ }
186
+
187
+ const contractRefusal = judgeAppStoreCatalogContract(envelope.catalog);
188
+ if (contractRefusal !== null) return { accepted: false, refusal: contractRefusal };
189
+ if (!isAppStoreCatalogEnvelope(inert, optionsArg)) {
190
+ return refuse('schemaInvalid', validateAppStoreCatalogEnvelope(inert, optionsArg).join('; '));
191
+ }
192
+ const mismatch = await findAppStoreCatalogConfigDigestMismatch(inert.catalog);
193
+ if (mismatch !== null) return refuse('digestMismatch', mismatch);
194
+ return { accepted: true, envelope: inert, keyId: verifiedKeyId };
195
+ };
package/ts/runtime.ts CHANGED
@@ -46,6 +46,7 @@ export * from './runtime.corestore.js';
46
46
  export * from './runtime.corestore.databaseexportpackage.js';
47
47
  export * from './runtime.corestore.relay.js';
48
48
  export * from './runtime.assignmentsecret.js';
49
+ export * from './runtime.appstorecatalog.js';
49
50
 
50
51
  export interface ISealedResolvedSecretMaterialEntry {
51
52
  secretVersionId: string;