@serve.zone/interfaces 32.48.0 → 32.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +47 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/catalog.d.ts +300 -0
- package/dist_ts/appstore/catalog.js +544 -0
- package/dist_ts/appstore/index.d.ts +2 -0
- package/dist_ts/appstore/index.js +3 -1
- package/dist_ts/appstore/types.d.ts +9 -0
- package/dist_ts/appstore/types.js +1 -1
- package/dist_ts/appstore/versionconfig.d.ts +21 -0
- package/dist_ts/appstore/versionconfig.js +685 -0
- package/dist_ts/private/keylist.d.ts +5 -0
- package/dist_ts/private/keylist.js +2 -0
- package/dist_ts/requests/appstore.d.ts +30 -1
- package/dist_ts/runtime.appstorecatalog.d.ts +46 -0
- package/dist_ts/runtime.appstorecatalog.js +142 -0
- package/dist_ts/runtime.d.ts +1 -0
- package/dist_ts/runtime.js +2 -1
- package/package.json +1 -1
- package/readme.md +57 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/catalog.ts +851 -0
- package/ts/appstore/index.ts +2 -0
- package/ts/appstore/types.ts +9 -0
- package/ts/appstore/versionconfig.ts +803 -0
- package/ts/private/keylist.ts +6 -0
- package/ts/requests/appstore.ts +38 -0
- package/ts/runtime.appstorecatalog.ts +195 -0
- package/ts/runtime.ts +1 -0
|
@@ -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;
|
package/ts/requests/appstore.ts
CHANGED
|
@@ -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
|
|
|
@@ -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;
|