@serve.zone/interfaces 21.1.0 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +23 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/types.d.ts +31 -16
- package/dist_ts/appstore/types.js +208 -11
- package/dist_ts/data/coremail.d.ts +83 -9
- package/dist_ts/data/coremail.js +67 -1
- package/dist_ts/data/coremail.runtime.d.ts +27 -1
- package/dist_ts/data/coremail.runtime.js +800 -36
- package/dist_ts/data/deploymentoperation.js +6 -20
- package/dist_ts/data/deploymentpreflight.d.ts +2 -6
- package/dist_ts/data/deploymentpreflight.js +1 -1
- package/dist_ts/data/hostedapp.d.ts +40 -14
- package/dist_ts/data/hostedapp.js +90 -1
- package/dist_ts/data/immutableimage.d.ts +30 -1
- package/dist_ts/data/immutableimage.js +103 -1
- package/dist_ts/data/index.d.ts +0 -2
- package/dist_ts/data/index.js +1 -3
- package/dist_ts/data/secret.d.ts +186 -13
- package/dist_ts/data/secret.js +543 -45
- package/dist_ts/data/service.d.ts +0 -39
- package/dist_ts/data/service.js +1 -77
- package/dist_ts/data/settings.d.ts +62 -43
- package/dist_ts/data/settings.js +34 -2
- package/dist_ts/platform/storage.d.ts +13 -14
- package/dist_ts/platform/storage.js +1 -1
- package/dist_ts/platform/storagemigration.d.ts +8 -8
- package/dist_ts/platform/storagemigration.golden.d.ts +2 -2
- package/dist_ts/platform/storagemigration.golden.js +19 -88
- package/dist_ts/platform/storagemigration.js +46 -58
- package/dist_ts/platform/types.d.ts +5 -19
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/requests/coremail.d.ts +12 -5
- package/dist_ts/requests/hostedapp.d.ts +12 -9
- package/dist_ts/requests/hostedapp.js +38 -1
- package/dist_ts/requests/index.d.ts +1 -3
- package/dist_ts/requests/index.js +2 -4
- package/dist_ts/requests/platform.d.ts +0 -1
- package/dist_ts/requests/secret.d.ts +23 -17
- package/dist_ts/requests/secret.js +161 -2
- package/dist_ts/requests/service.d.ts +0 -13
- package/dist_ts/requests/settings.d.ts +3 -3
- package/dist_ts/runtime.d.ts +78 -26
- package/dist_ts/runtime.js +219 -19
- package/package.json +2 -1
- package/readme.md +163 -38
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/types.ts +251 -25
- package/ts/data/coremail.runtime.ts +1212 -38
- package/ts/data/coremail.ts +120 -8
- package/ts/data/deploymentoperation.ts +7 -31
- package/ts/data/deploymentpreflight.ts +2 -5
- package/ts/data/hostedapp.ts +144 -18
- package/ts/data/immutableimage.ts +150 -1
- package/ts/data/index.ts +0 -2
- package/ts/data/secret.ts +775 -53
- package/ts/data/service.ts +0 -118
- package/ts/data/settings.ts +70 -80
- package/ts/platform/storage.ts +15 -16
- package/ts/platform/storagemigration.golden.ts +18 -95
- package/ts/platform/storagemigration.ts +57 -87
- package/ts/platform/types.ts +6 -19
- package/ts/plugins.ts +2 -0
- package/ts/requests/coremail.ts +13 -4
- package/ts/requests/hostedapp.ts +50 -9
- package/ts/requests/index.ts +0 -4
- package/ts/requests/platform.ts +0 -1
- package/ts/requests/secret.ts +204 -9
- package/ts/requests/service.ts +0 -16
- package/ts/requests/settings.ts +3 -3
- package/ts/runtime.ts +314 -41
- package/dist_ts/data/secretbundle.d.ts +0 -57
- package/dist_ts/data/secretbundle.js +0 -2
- package/dist_ts/data/secretgroup.d.ts +0 -52
- package/dist_ts/data/secretgroup.js +0 -2
- package/dist_ts/requests/secretbundle.d.ts +0 -75
- package/dist_ts/requests/secretbundle.js +0 -4
- package/dist_ts/requests/secretgroup.d.ts +0 -52
- package/dist_ts/requests/secretgroup.js +0 -4
- package/ts/data/secretbundle.ts +0 -65
- package/ts/data/secretgroup.ts +0 -58
- package/ts/requests/secretbundle.ts +0 -103
- package/ts/requests/secretgroup.ts +0 -74
package/ts/data/secret.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
1
2
|
import type { TSha256Digest } from './immutableimage.js';
|
|
2
3
|
import { normalizeServiceAbsolutePath } from './service.js';
|
|
3
4
|
|
|
@@ -18,11 +19,12 @@ export type TSecretVersionLifecycleState =
|
|
|
18
19
|
/** Retired versions remain usable only through an already-accepted manifest. */
|
|
19
20
|
export const isSecretVersionMaterialIssuable = (
|
|
20
21
|
lifecycleStateArg: TSecretVersionLifecycleState,
|
|
21
|
-
|
|
22
|
+
referencedByAcceptedManifestArg: boolean,
|
|
23
|
+
): boolean => lifecycleStateArg === 'active'
|
|
24
|
+
|| (lifecycleStateArg === 'retired' && referencedByAcceptedManifestArg);
|
|
22
25
|
|
|
23
26
|
export type TSecretManagementSource =
|
|
24
27
|
| 'operator'
|
|
25
|
-
| 'legacy'
|
|
26
28
|
| 'appstore'
|
|
27
29
|
| 'mail'
|
|
28
30
|
| 'oidc'
|
|
@@ -32,7 +34,6 @@ export type TSecretManagementSource =
|
|
|
32
34
|
|
|
33
35
|
export type TSecretManagementScope =
|
|
34
36
|
| 'operator'
|
|
35
|
-
| 'legacy'
|
|
36
37
|
| `appstore:${string}`
|
|
37
38
|
| `mail:${string}`
|
|
38
39
|
| `oidc:${string}`
|
|
@@ -50,7 +51,24 @@ export interface ISecretSetSecretOwner {
|
|
|
50
51
|
secretSetId: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export
|
|
54
|
+
export interface IPlatformProviderSecretOwner {
|
|
55
|
+
kind: 'platform-provider';
|
|
56
|
+
providerConfigId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ISystemSecretOwner {
|
|
60
|
+
kind: 'system';
|
|
61
|
+
systemId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type TSecretOwner =
|
|
65
|
+
| IServiceSecretOwner
|
|
66
|
+
| ISecretSetSecretOwner
|
|
67
|
+
| IPlatformProviderSecretOwner
|
|
68
|
+
| ISystemSecretOwner;
|
|
69
|
+
|
|
70
|
+
export type TSecretMutationTarget = TSecretOwner;
|
|
71
|
+
export type TWorkloadSecretOwner = IServiceSecretOwner | ISecretSetSecretOwner;
|
|
54
72
|
|
|
55
73
|
export interface ISecretVersionReference {
|
|
56
74
|
secretId: string;
|
|
@@ -146,9 +164,12 @@ export interface ISecretSetConsumerRolloutStatus {
|
|
|
146
164
|
updatedAt: number;
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
export interface
|
|
150
|
-
type: 'environment';
|
|
167
|
+
export interface ISecretLauncherEnvironmentDelivery {
|
|
168
|
+
type: 'launcher-environment';
|
|
151
169
|
variableName: string;
|
|
170
|
+
uid: number;
|
|
171
|
+
gid: number;
|
|
172
|
+
mode: 0o400 | 0o440;
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
export interface ISecretFileDelivery {
|
|
@@ -156,10 +177,29 @@ export interface ISecretFileDelivery {
|
|
|
156
177
|
targetPath: string;
|
|
157
178
|
uid: number;
|
|
158
179
|
gid: number;
|
|
159
|
-
mode:
|
|
180
|
+
mode: 0o400 | 0o440;
|
|
160
181
|
}
|
|
161
182
|
|
|
162
|
-
export type TSecretDelivery =
|
|
183
|
+
export type TSecretDelivery = ISecretLauncherEnvironmentDelivery | ISecretFileDelivery;
|
|
184
|
+
|
|
185
|
+
export const secretValueLimits = Object.freeze({
|
|
186
|
+
maximumBytes: 500 * 1024,
|
|
187
|
+
minimumGeneratedBytes: 16,
|
|
188
|
+
maximumGeneratedBytes: 64,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export type TSecretGeneratedEncoding = 'hex' | 'base64url';
|
|
192
|
+
|
|
193
|
+
export type TSecretValueInput =
|
|
194
|
+
| {
|
|
195
|
+
mode: 'sealed';
|
|
196
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
197
|
+
}
|
|
198
|
+
| {
|
|
199
|
+
mode: 'generated';
|
|
200
|
+
encoding: TSecretGeneratedEncoding;
|
|
201
|
+
bytes: number;
|
|
202
|
+
};
|
|
163
203
|
|
|
164
204
|
export interface ISecretSetAttachmentMapping {
|
|
165
205
|
/** Source key in the attached SecretSet. */
|
|
@@ -182,7 +222,7 @@ export interface ISecretSetAttachment {
|
|
|
182
222
|
* Cloudly verifies organization ownership when resolving the referenced sets.
|
|
183
223
|
*/
|
|
184
224
|
export interface IServiceSecretConfiguration {
|
|
185
|
-
schemaVersion:
|
|
225
|
+
schemaVersion: 2;
|
|
186
226
|
/** Dedicated CAS revision for SecretSet attachments. */
|
|
187
227
|
revision: number;
|
|
188
228
|
/** Dedicated CAS revision for service-owned secret membership. */
|
|
@@ -195,18 +235,22 @@ export interface IResolvedSecretManifestEntry {
|
|
|
195
235
|
secretVersionId: string;
|
|
196
236
|
key: string;
|
|
197
237
|
environment: TSecretEnvironment;
|
|
198
|
-
owner:
|
|
238
|
+
owner: TWorkloadSecretOwner;
|
|
199
239
|
delivery: TSecretDelivery;
|
|
200
240
|
}
|
|
201
241
|
|
|
202
242
|
/** Immutable, value-free runtime snapshot. */
|
|
203
243
|
export interface IResolvedSecretManifest {
|
|
204
|
-
schemaVersion:
|
|
244
|
+
schemaVersion: 2;
|
|
205
245
|
id: string;
|
|
206
246
|
digest: TSha256Digest;
|
|
207
247
|
organizationId: string;
|
|
208
248
|
serviceId: string;
|
|
209
249
|
clusterId: string;
|
|
250
|
+
imageRolloutId: string;
|
|
251
|
+
imageRolloutGeneration: number;
|
|
252
|
+
requestedImageDigest: TSha256Digest;
|
|
253
|
+
invocationDigest: TSha256Digest;
|
|
210
254
|
secretRolloutId: string;
|
|
211
255
|
secretRolloutGeneration: number;
|
|
212
256
|
entries: IResolvedSecretManifestEntry[];
|
|
@@ -217,6 +261,10 @@ export interface IResolvedSecretManifestReference {
|
|
|
217
261
|
manifestId: string;
|
|
218
262
|
manifestDigest: TSha256Digest;
|
|
219
263
|
clusterId: string;
|
|
264
|
+
imageRolloutId: string;
|
|
265
|
+
imageRolloutGeneration: number;
|
|
266
|
+
requestedImageDigest: TSha256Digest;
|
|
267
|
+
invocationDigest: TSha256Digest;
|
|
220
268
|
secretRolloutId: string;
|
|
221
269
|
secretRolloutGeneration: number;
|
|
222
270
|
}
|
|
@@ -245,7 +293,7 @@ export interface IClusterSecretDeploymentState {
|
|
|
245
293
|
|
|
246
294
|
/** Server-managed per-cluster authority switched with an expected revision CAS. */
|
|
247
295
|
export interface IServiceSecretDeploymentPlan {
|
|
248
|
-
schemaVersion:
|
|
296
|
+
schemaVersion: 2;
|
|
249
297
|
revision: number;
|
|
250
298
|
clusterStates: IClusterSecretDeploymentState[];
|
|
251
299
|
updatedAt: number;
|
|
@@ -260,6 +308,10 @@ export const resolvedSecretManifestReferencesEqual = (
|
|
|
260
308
|
&& leftArg?.manifestId === rightArg?.manifestId
|
|
261
309
|
&& leftArg?.manifestDigest === rightArg?.manifestDigest
|
|
262
310
|
&& leftArg?.clusterId === rightArg?.clusterId
|
|
311
|
+
&& leftArg?.imageRolloutId === rightArg?.imageRolloutId
|
|
312
|
+
&& leftArg?.imageRolloutGeneration === rightArg?.imageRolloutGeneration
|
|
313
|
+
&& leftArg?.requestedImageDigest === rightArg?.requestedImageDigest
|
|
314
|
+
&& leftArg?.invocationDigest === rightArg?.invocationDigest
|
|
263
315
|
&& leftArg?.secretRolloutId === rightArg?.secretRolloutId
|
|
264
316
|
&& leftArg?.secretRolloutGeneration === rightArg?.secretRolloutGeneration
|
|
265
317
|
);
|
|
@@ -292,6 +344,20 @@ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
|
|
|
292
344
|
Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg)
|
|
293
345
|
);
|
|
294
346
|
|
|
347
|
+
const hasExactKeys = (
|
|
348
|
+
valueArg: Record<string, unknown>,
|
|
349
|
+
keysArg: string[],
|
|
350
|
+
): boolean => {
|
|
351
|
+
const actual = Object.keys(valueArg).sort();
|
|
352
|
+
const expected = [...keysArg].sort();
|
|
353
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const hasOnlyKeys = (
|
|
357
|
+
valueArg: Record<string, unknown>,
|
|
358
|
+
keysArg: string[],
|
|
359
|
+
): boolean => Object.keys(valueArg).every((keyArg) => keysArg.includes(keyArg));
|
|
360
|
+
|
|
295
361
|
const isSecretIdentifier = (valueArg: unknown): valueArg is string => (
|
|
296
362
|
typeof valueArg === 'string' && secretIdentifierRegex.test(valueArg)
|
|
297
363
|
);
|
|
@@ -304,6 +370,30 @@ const isSecretEnvironment = (valueArg: unknown): valueArg is string => (
|
|
|
304
370
|
typeof valueArg === 'string' && secretEnvironmentRegex.test(valueArg)
|
|
305
371
|
);
|
|
306
372
|
|
|
373
|
+
export const validateSecretMutationId = (mutationIdArg: unknown): string[] => (
|
|
374
|
+
isSecretIdentifier(mutationIdArg)
|
|
375
|
+
? []
|
|
376
|
+
: ['mutationId must be a canonical identifier']
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
export const validateSecretOwner = (ownerArg: unknown): string[] => {
|
|
380
|
+
if (!isRecord(ownerArg)) return ['secret owner must be an object'];
|
|
381
|
+
const shapeByKind = {
|
|
382
|
+
service: ['kind', 'serviceId'],
|
|
383
|
+
'secret-set': ['kind', 'secretSetId'],
|
|
384
|
+
'platform-provider': ['kind', 'providerConfigId'],
|
|
385
|
+
system: ['kind', 'systemId'],
|
|
386
|
+
} as const;
|
|
387
|
+
const keys = shapeByKind[ownerArg.kind as keyof typeof shapeByKind];
|
|
388
|
+
if (!keys || !hasExactKeys(ownerArg, [...keys])) {
|
|
389
|
+
return ['secret owner must use an exact supported owner schema'];
|
|
390
|
+
}
|
|
391
|
+
const idField = keys[1];
|
|
392
|
+
return isSecretIdentifier(ownerArg[idField])
|
|
393
|
+
? []
|
|
394
|
+
: [`secret owner ${idField} must be canonical`];
|
|
395
|
+
};
|
|
396
|
+
|
|
307
397
|
export const validateSecretManagementScope = (
|
|
308
398
|
sourceArg: TSecretManagementSource,
|
|
309
399
|
scopeArg: unknown,
|
|
@@ -313,11 +403,6 @@ export const validateSecretManagementScope = (
|
|
|
313
403
|
? []
|
|
314
404
|
: ['operator management scope requires operator source'];
|
|
315
405
|
}
|
|
316
|
-
if (scopeArg === 'legacy') {
|
|
317
|
-
return sourceArg === 'legacy'
|
|
318
|
-
? []
|
|
319
|
-
: ['legacy management scope requires legacy source'];
|
|
320
|
-
}
|
|
321
406
|
if (typeof scopeArg !== 'string' || !secretManagementScopeRegex.test(scopeArg)) {
|
|
322
407
|
return ['management scope must be canonical'];
|
|
323
408
|
}
|
|
@@ -328,19 +413,23 @@ export const validateSecretManagementScope = (
|
|
|
328
413
|
|
|
329
414
|
export const validateSecretDelivery = (deliveryArg: unknown): string[] => {
|
|
330
415
|
if (!isRecord(deliveryArg)) return ['delivery must be an object'];
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
: ['environment delivery variableName must be a canonical environment key'];
|
|
416
|
+
const isLauncher = deliveryArg.type === 'launcher-environment';
|
|
417
|
+
if (!isLauncher && deliveryArg.type !== 'file') {
|
|
418
|
+
return ['delivery type must be launcher-environment or file'];
|
|
335
419
|
}
|
|
336
|
-
|
|
337
|
-
|
|
420
|
+
const expectedKeys = isLauncher
|
|
421
|
+
? ['type', 'variableName', 'uid', 'gid', 'mode']
|
|
422
|
+
: ['type', 'targetPath', 'uid', 'gid', 'mode'];
|
|
423
|
+
if (!hasExactKeys(deliveryArg, expectedKeys)) {
|
|
424
|
+
return ['delivery must use its exact schema'];
|
|
338
425
|
}
|
|
339
426
|
const errors: string[] = [];
|
|
340
|
-
if (
|
|
427
|
+
if (isLauncher && !isSecretKey(deliveryArg.variableName)) {
|
|
428
|
+
errors.push('launcher delivery variableName must be a canonical environment key');
|
|
429
|
+
}
|
|
430
|
+
if (!isLauncher && (typeof deliveryArg.targetPath !== 'string'
|
|
341
431
|
|| normalizeServiceAbsolutePath(deliveryArg.targetPath) !== deliveryArg.targetPath
|
|
342
|
-
|| !/^\/run\/secrets\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(deliveryArg.targetPath)
|
|
343
|
-
|| deliveryArg.targetPath === '/run/secrets/secret.json') {
|
|
432
|
+
|| !/^\/run\/secrets\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(deliveryArg.targetPath))) {
|
|
344
433
|
errors.push('file delivery targetPath must be a canonical file below /run/secrets');
|
|
345
434
|
}
|
|
346
435
|
for (const field of ['uid', 'gid'] as const) {
|
|
@@ -348,15 +437,97 @@ export const validateSecretDelivery = (deliveryArg: unknown): string[] => {
|
|
|
348
437
|
if (!Number.isSafeInteger(value)
|
|
349
438
|
|| (value as number) < 0
|
|
350
439
|
|| (value as number) > 2_147_483_647) {
|
|
351
|
-
errors.push(
|
|
440
|
+
errors.push(`${isLauncher ? 'launcher' : 'file'} delivery ${field} must be a bounded numeric identity`);
|
|
352
441
|
}
|
|
353
442
|
}
|
|
354
443
|
if (deliveryArg.mode !== 0o400 && deliveryArg.mode !== 0o440) {
|
|
355
|
-
errors.push('file delivery mode must be 0400 or 0440
|
|
444
|
+
errors.push(`${isLauncher ? 'launcher' : 'file'} delivery mode must be 0400 or 0440`);
|
|
356
445
|
}
|
|
357
446
|
return errors;
|
|
358
447
|
};
|
|
359
448
|
|
|
449
|
+
export const validateSecretValueInput = (valueInputArg: unknown): string[] => {
|
|
450
|
+
if (!isRecord(valueInputArg)) return ['valueInput must be an object'];
|
|
451
|
+
if (valueInputArg.mode === 'sealed') {
|
|
452
|
+
if (!hasExactKeys(valueInputArg, ['mode', 'envelope'])) {
|
|
453
|
+
return ['sealed valueInput must use its exact schema'];
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(valueInputArg.envelope);
|
|
457
|
+
return envelope.ciphertext.length
|
|
458
|
+
<= Math.ceil(secretValueLimits.maximumBytes * 4 / 3)
|
|
459
|
+
? []
|
|
460
|
+
: [`sealed secret material must not exceed ${secretValueLimits.maximumBytes} bytes`];
|
|
461
|
+
} catch {
|
|
462
|
+
return ['sealed valueInput envelope must be a strict X25519 envelope'];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (valueInputArg.mode === 'generated') {
|
|
466
|
+
if (!hasExactKeys(valueInputArg, ['mode', 'encoding', 'bytes'])) {
|
|
467
|
+
return ['generated valueInput must use its exact schema'];
|
|
468
|
+
}
|
|
469
|
+
const errors: string[] = [];
|
|
470
|
+
if (valueInputArg.encoding !== 'hex' && valueInputArg.encoding !== 'base64url') {
|
|
471
|
+
errors.push('generated valueInput encoding must be hex or base64url');
|
|
472
|
+
}
|
|
473
|
+
if (!Number.isSafeInteger(valueInputArg.bytes)
|
|
474
|
+
|| (valueInputArg.bytes as number) < secretValueLimits.minimumGeneratedBytes
|
|
475
|
+
|| (valueInputArg.bytes as number) > secretValueLimits.maximumGeneratedBytes) {
|
|
476
|
+
errors.push('generated valueInput bytes are outside the supported bounds');
|
|
477
|
+
}
|
|
478
|
+
return errors;
|
|
479
|
+
}
|
|
480
|
+
return ['valueInput mode must be sealed or generated'];
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const encodeBase64Url = (bytesArg: Uint8Array): string => {
|
|
484
|
+
let binary = '';
|
|
485
|
+
for (const byte of bytesArg) binary += String.fromCharCode(byte);
|
|
486
|
+
return globalThis.btoa(binary)
|
|
487
|
+
.replace(/\+/g, '-')
|
|
488
|
+
.replace(/\//g, '_')
|
|
489
|
+
.replace(/=+$/, '');
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/** Checks the SmartCrypto context digest without opening or decrypting the envelope. */
|
|
493
|
+
export const verifySecretEnvelopeContext = async (
|
|
494
|
+
envelopeArg: unknown,
|
|
495
|
+
contextArg: unknown,
|
|
496
|
+
): Promise<boolean> => {
|
|
497
|
+
if (!(contextArg instanceof Uint8Array)) return false;
|
|
498
|
+
let envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
499
|
+
try {
|
|
500
|
+
envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const contextDigest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
505
|
+
'SHA-256',
|
|
506
|
+
new Uint8Array(contextArg),
|
|
507
|
+
));
|
|
508
|
+
return envelope.contextDigest === encodeBase64Url(contextDigest);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/** Files accept arbitrary bytes; launcher values must be valid UTF-8 without NUL. */
|
|
512
|
+
export const validateSecretValueBytes = (
|
|
513
|
+
valueBytesArg: unknown,
|
|
514
|
+
deliveryArg: TSecretDelivery,
|
|
515
|
+
): string[] => {
|
|
516
|
+
if (!(valueBytesArg instanceof Uint8Array)) return ['secret material must be bytes'];
|
|
517
|
+
if (valueBytesArg.byteLength > secretValueLimits.maximumBytes) {
|
|
518
|
+
return [`secret material must not exceed ${secretValueLimits.maximumBytes} bytes`];
|
|
519
|
+
}
|
|
520
|
+
if (deliveryArg.type === 'file') return [];
|
|
521
|
+
try {
|
|
522
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(valueBytesArg);
|
|
523
|
+
return decoded.includes('\0')
|
|
524
|
+
? ['launcher secret material must not contain NUL']
|
|
525
|
+
: [];
|
|
526
|
+
} catch {
|
|
527
|
+
return ['launcher secret material must be valid UTF-8'];
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
360
531
|
/**
|
|
361
532
|
* One shared precedence rule for resolution and runtime verification.
|
|
362
533
|
* A manifest is authoritative once published; before publication an attachment
|
|
@@ -378,16 +549,26 @@ export const validateServiceSecretConfiguration = (
|
|
|
378
549
|
if (!isRecord(configurationArg)) {
|
|
379
550
|
return ['secretConfiguration must be an object'];
|
|
380
551
|
}
|
|
381
|
-
|
|
382
|
-
|
|
552
|
+
const errors: string[] = [];
|
|
553
|
+
if (!hasExactKeys(configurationArg, [
|
|
554
|
+
'schemaVersion',
|
|
555
|
+
'revision',
|
|
556
|
+
'secretsRevision',
|
|
557
|
+
'secretSetAttachments',
|
|
558
|
+
])) {
|
|
559
|
+
errors.push('secretConfiguration must use its exact schema');
|
|
560
|
+
}
|
|
561
|
+
if (configurationArg.schemaVersion !== 2) {
|
|
562
|
+
errors.push('secretConfiguration schemaVersion must be 2');
|
|
383
563
|
}
|
|
384
564
|
if (!Array.isArray(configurationArg.secretSetAttachments)) {
|
|
385
|
-
|
|
565
|
+
errors.push('secretSetAttachments must be an array');
|
|
566
|
+
return errors;
|
|
386
567
|
}
|
|
387
568
|
if (configurationArg.secretSetAttachments.length > 64) {
|
|
388
|
-
|
|
569
|
+
errors.push('secretSetAttachments must contain at most 64 entries');
|
|
570
|
+
return errors;
|
|
389
571
|
}
|
|
390
|
-
const errors: string[] = [];
|
|
391
572
|
if (!Number.isSafeInteger(configurationArg.revision)
|
|
392
573
|
|| (configurationArg.revision as number) < 0) {
|
|
393
574
|
errors.push('secretConfiguration revision must be a non-negative integer');
|
|
@@ -403,6 +584,9 @@ export const validateServiceSecretConfiguration = (
|
|
|
403
584
|
errors.push(`secretSetAttachments[${attachmentIndex}] must be an object`);
|
|
404
585
|
continue;
|
|
405
586
|
}
|
|
587
|
+
if (!hasOnlyKeys(candidate, ['id', 'secretSetId', 'environment', 'mappings'])) {
|
|
588
|
+
errors.push(`secretSetAttachments[${attachmentIndex}] contains unsupported fields`);
|
|
589
|
+
}
|
|
406
590
|
if (!isSecretIdentifier(candidate.id)) {
|
|
407
591
|
errors.push(`secretSetAttachments[${attachmentIndex}].id must be a canonical identifier`);
|
|
408
592
|
} else {
|
|
@@ -436,6 +620,11 @@ export const validateServiceSecretConfiguration = (
|
|
|
436
620
|
);
|
|
437
621
|
continue;
|
|
438
622
|
}
|
|
623
|
+
if (!hasOnlyKeys(mappingCandidate, ['sourceKey', 'targetKey', 'delivery'])) {
|
|
624
|
+
errors.push(
|
|
625
|
+
`secretSetAttachments[${attachmentIndex}].mappings[${mappingIndex}] contains unsupported fields`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
439
628
|
if (!isSecretKey(mappingCandidate.sourceKey)) {
|
|
440
629
|
errors.push(
|
|
441
630
|
`secretSetAttachments[${attachmentIndex}].mappings[${mappingIndex}].sourceKey must be canonical`,
|
|
@@ -475,21 +664,6 @@ export const validateServiceSecretConfiguration = (
|
|
|
475
664
|
return errors;
|
|
476
665
|
};
|
|
477
666
|
|
|
478
|
-
export const validateServiceSecretMode = (serviceDataArg: unknown): string[] => {
|
|
479
|
-
if (!isRecord(serviceDataArg)) return ['service data must be an object'];
|
|
480
|
-
const hasLegacyPrimary = typeof serviceDataArg.secretBundleId === 'string'
|
|
481
|
-
&& serviceDataArg.secretBundleId.length > 0;
|
|
482
|
-
const hasLegacyAdditional = Array.isArray(serviceDataArg.additionalSecretBundleIds)
|
|
483
|
-
&& serviceDataArg.additionalSecretBundleIds.length > 0;
|
|
484
|
-
if (serviceDataArg.secretConfiguration !== undefined
|
|
485
|
-
&& (hasLegacyPrimary || hasLegacyAdditional)) {
|
|
486
|
-
return ['legacy SecretBundle references cannot be mixed with secretConfiguration'];
|
|
487
|
-
}
|
|
488
|
-
return serviceDataArg.secretConfiguration === undefined
|
|
489
|
-
? []
|
|
490
|
-
: validateServiceSecretConfiguration(serviceDataArg.secretConfiguration);
|
|
491
|
-
};
|
|
492
|
-
|
|
493
667
|
export const validateSecretSetAttachmentOwnership = (
|
|
494
668
|
configurationArg: IServiceSecretConfiguration,
|
|
495
669
|
serviceOrganizationIdArg: string,
|
|
@@ -521,6 +695,10 @@ export const createResolvedSecretManifestDigestInput = (
|
|
|
521
695
|
organizationId: manifestArg.organizationId,
|
|
522
696
|
serviceId: manifestArg.serviceId,
|
|
523
697
|
clusterId: manifestArg.clusterId,
|
|
698
|
+
imageRolloutId: manifestArg.imageRolloutId,
|
|
699
|
+
imageRolloutGeneration: manifestArg.imageRolloutGeneration,
|
|
700
|
+
requestedImageDigest: manifestArg.requestedImageDigest,
|
|
701
|
+
invocationDigest: manifestArg.invocationDigest,
|
|
524
702
|
secretRolloutId: manifestArg.secretRolloutId,
|
|
525
703
|
secretRolloutGeneration: manifestArg.secretRolloutGeneration,
|
|
526
704
|
entries: manifestArg.entries.map((entryArg) => ({
|
|
@@ -537,10 +715,13 @@ export const createResolvedSecretManifestDigestInput = (
|
|
|
537
715
|
kind: 'secret-set',
|
|
538
716
|
secretSetId: entryArg.owner.secretSetId,
|
|
539
717
|
},
|
|
540
|
-
delivery: entryArg.delivery.type === 'environment'
|
|
718
|
+
delivery: entryArg.delivery.type === 'launcher-environment'
|
|
541
719
|
? {
|
|
542
|
-
type: 'environment',
|
|
720
|
+
type: 'launcher-environment',
|
|
543
721
|
variableName: entryArg.delivery.variableName,
|
|
722
|
+
uid: entryArg.delivery.uid,
|
|
723
|
+
gid: entryArg.delivery.gid,
|
|
724
|
+
mode: entryArg.delivery.mode,
|
|
544
725
|
}
|
|
545
726
|
: {
|
|
546
727
|
type: 'file',
|
|
@@ -576,12 +757,31 @@ export const validateResolvedSecretManifest = (
|
|
|
576
757
|
): string[] => {
|
|
577
758
|
if (!isRecord(manifestArg)) return ['secret manifest must be an object'];
|
|
578
759
|
const errors: string[] = [];
|
|
579
|
-
if (manifestArg
|
|
760
|
+
if (!hasExactKeys(manifestArg, [
|
|
761
|
+
'schemaVersion',
|
|
762
|
+
'id',
|
|
763
|
+
'digest',
|
|
764
|
+
'organizationId',
|
|
765
|
+
'serviceId',
|
|
766
|
+
'clusterId',
|
|
767
|
+
'imageRolloutId',
|
|
768
|
+
'imageRolloutGeneration',
|
|
769
|
+
'requestedImageDigest',
|
|
770
|
+
'invocationDigest',
|
|
771
|
+
'secretRolloutId',
|
|
772
|
+
'secretRolloutGeneration',
|
|
773
|
+
'entries',
|
|
774
|
+
'createdAt',
|
|
775
|
+
])) {
|
|
776
|
+
errors.push('secret manifest must use its exact schema');
|
|
777
|
+
}
|
|
778
|
+
if (manifestArg.schemaVersion !== 2) errors.push('secret manifest schemaVersion must be 2');
|
|
580
779
|
for (const field of [
|
|
581
780
|
'id',
|
|
582
781
|
'organizationId',
|
|
583
782
|
'serviceId',
|
|
584
783
|
'clusterId',
|
|
784
|
+
'imageRolloutId',
|
|
585
785
|
'secretRolloutId',
|
|
586
786
|
] as const) {
|
|
587
787
|
if (!isSecretIdentifier(manifestArg[field])) {
|
|
@@ -591,6 +791,15 @@ export const validateResolvedSecretManifest = (
|
|
|
591
791
|
if (typeof manifestArg.digest !== 'string' || !secretDigestRegex.test(manifestArg.digest)) {
|
|
592
792
|
errors.push('secret manifest digest must be a canonical sha256 digest');
|
|
593
793
|
}
|
|
794
|
+
for (const field of ['requestedImageDigest', 'invocationDigest'] as const) {
|
|
795
|
+
if (typeof manifestArg[field] !== 'string' || !secretDigestRegex.test(manifestArg[field])) {
|
|
796
|
+
errors.push(`secret manifest ${field} must be a canonical sha256 digest`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!Number.isSafeInteger(manifestArg.imageRolloutGeneration)
|
|
800
|
+
|| (manifestArg.imageRolloutGeneration as number) < 1) {
|
|
801
|
+
errors.push('secret manifest image rollout generation must be a positive integer');
|
|
802
|
+
}
|
|
594
803
|
if (!Number.isSafeInteger(manifestArg.secretRolloutGeneration)
|
|
595
804
|
|| (manifestArg.secretRolloutGeneration as number) < 1) {
|
|
596
805
|
errors.push('secret manifest rollout generation must be a positive integer');
|
|
@@ -613,6 +822,16 @@ export const validateResolvedSecretManifest = (
|
|
|
613
822
|
errors.push(`secret manifest entries[${index}] must be an object`);
|
|
614
823
|
continue;
|
|
615
824
|
}
|
|
825
|
+
if (!hasExactKeys(candidate, [
|
|
826
|
+
'secretId',
|
|
827
|
+
'secretVersionId',
|
|
828
|
+
'key',
|
|
829
|
+
'environment',
|
|
830
|
+
'owner',
|
|
831
|
+
'delivery',
|
|
832
|
+
])) {
|
|
833
|
+
errors.push(`secret manifest entries[${index}] must use its exact schema`);
|
|
834
|
+
}
|
|
616
835
|
if (!isSecretIdentifier(candidate.secretId)) {
|
|
617
836
|
errors.push(`secret manifest entries[${index}].secretId must be canonical`);
|
|
618
837
|
} else {
|
|
@@ -636,6 +855,12 @@ export const validateResolvedSecretManifest = (
|
|
|
636
855
|
candidate.owner.kind !== 'service'
|
|
637
856
|
&& candidate.owner.kind !== 'secret-set'
|
|
638
857
|
)
|
|
858
|
+
|| !hasExactKeys(
|
|
859
|
+
candidate.owner,
|
|
860
|
+
candidate.owner.kind === 'service'
|
|
861
|
+
? ['kind', 'serviceId']
|
|
862
|
+
: ['kind', 'secretSetId'],
|
|
863
|
+
)
|
|
639
864
|
|| (
|
|
640
865
|
candidate.owner.kind === 'service'
|
|
641
866
|
&& !isSecretIdentifier(candidate.owner.serviceId)
|
|
@@ -659,7 +884,7 @@ export const validateResolvedSecretManifest = (
|
|
|
659
884
|
));
|
|
660
885
|
if (isRecord(candidate.delivery)) {
|
|
661
886
|
if (
|
|
662
|
-
candidate.delivery.type === 'environment'
|
|
887
|
+
candidate.delivery.type === 'launcher-environment'
|
|
663
888
|
&& isSecretKey(candidate.delivery.variableName)
|
|
664
889
|
) {
|
|
665
890
|
environmentDeliveryTargets.push(candidate.delivery.variableName);
|
|
@@ -703,10 +928,28 @@ const validateResolvedSecretManifestReference = (
|
|
|
703
928
|
pathArg: string,
|
|
704
929
|
): string[] => {
|
|
705
930
|
if (!isRecord(referenceArg)
|
|
931
|
+
|| !hasExactKeys(referenceArg, [
|
|
932
|
+
'manifestId',
|
|
933
|
+
'manifestDigest',
|
|
934
|
+
'clusterId',
|
|
935
|
+
'imageRolloutId',
|
|
936
|
+
'imageRolloutGeneration',
|
|
937
|
+
'requestedImageDigest',
|
|
938
|
+
'invocationDigest',
|
|
939
|
+
'secretRolloutId',
|
|
940
|
+
'secretRolloutGeneration',
|
|
941
|
+
])
|
|
706
942
|
|| !isSecretIdentifier(referenceArg.manifestId)
|
|
707
943
|
|| typeof referenceArg.manifestDigest !== 'string'
|
|
708
944
|
|| !secretDigestRegex.test(referenceArg.manifestDigest)
|
|
709
945
|
|| referenceArg.clusterId !== expectedClusterIdArg
|
|
946
|
+
|| !isSecretIdentifier(referenceArg.imageRolloutId)
|
|
947
|
+
|| !Number.isSafeInteger(referenceArg.imageRolloutGeneration)
|
|
948
|
+
|| (referenceArg.imageRolloutGeneration as number) < 1
|
|
949
|
+
|| typeof referenceArg.requestedImageDigest !== 'string'
|
|
950
|
+
|| !secretDigestRegex.test(referenceArg.requestedImageDigest)
|
|
951
|
+
|| typeof referenceArg.invocationDigest !== 'string'
|
|
952
|
+
|| !secretDigestRegex.test(referenceArg.invocationDigest)
|
|
710
953
|
|| !isSecretIdentifier(referenceArg.secretRolloutId)
|
|
711
954
|
|| !Number.isSafeInteger(referenceArg.secretRolloutGeneration)
|
|
712
955
|
|| (referenceArg.secretRolloutGeneration as number) < 1) {
|
|
@@ -721,7 +964,15 @@ export const validateServiceSecretDeploymentPlan = (
|
|
|
721
964
|
): string[] => {
|
|
722
965
|
if (!isRecord(planArg)) return ['secret deployment plan must be an object'];
|
|
723
966
|
const errors: string[] = [];
|
|
724
|
-
if (planArg
|
|
967
|
+
if (!hasExactKeys(planArg, [
|
|
968
|
+
'schemaVersion',
|
|
969
|
+
'revision',
|
|
970
|
+
'clusterStates',
|
|
971
|
+
'updatedAt',
|
|
972
|
+
])) {
|
|
973
|
+
errors.push('secret deployment plan must use its exact schema');
|
|
974
|
+
}
|
|
975
|
+
if (planArg.schemaVersion !== 2) errors.push('secret deployment plan schemaVersion must be 2');
|
|
725
976
|
if (!Number.isSafeInteger(planArg.revision) || (planArg.revision as number) < 1) {
|
|
726
977
|
errors.push('secret deployment plan revision must be a positive integer');
|
|
727
978
|
}
|
|
@@ -767,6 +1018,16 @@ export const validateServiceSecretDeploymentPlan = (
|
|
|
767
1018
|
errors.push(`${path}.clusterId must be canonical`);
|
|
768
1019
|
continue;
|
|
769
1020
|
}
|
|
1021
|
+
if (!hasOnlyKeys(candidate, [
|
|
1022
|
+
'clusterId',
|
|
1023
|
+
'desiredManifest',
|
|
1024
|
+
'appliedManifest',
|
|
1025
|
+
'previousAcceptedManifest',
|
|
1026
|
+
'status',
|
|
1027
|
+
'updatedAt',
|
|
1028
|
+
])) {
|
|
1029
|
+
errors.push(`${path} contains unsupported fields`);
|
|
1030
|
+
}
|
|
770
1031
|
clusterIds.push(candidate.clusterId);
|
|
771
1032
|
validateAndTrackManifestReference(
|
|
772
1033
|
candidate.desiredManifest,
|
|
@@ -782,6 +1043,7 @@ export const validateServiceSecretDeploymentPlan = (
|
|
|
782
1043
|
}
|
|
783
1044
|
if (candidate.previousAcceptedManifest !== undefined) {
|
|
784
1045
|
if (!isRecord(candidate.previousAcceptedManifest)
|
|
1046
|
+
|| !hasExactKeys(candidate.previousAcceptedManifest, ['manifest', 'acceptedUntil'])
|
|
785
1047
|
|| !Number.isSafeInteger(candidate.previousAcceptedManifest.acceptedUntil)
|
|
786
1048
|
|| (candidate.previousAcceptedManifest.acceptedUntil as number) < 0) {
|
|
787
1049
|
errors.push(`${path}.previousAcceptedManifest acceptedUntil must be non-negative`);
|
|
@@ -833,3 +1095,463 @@ export const validateServiceSecretDeploymentPlan = (
|
|
|
833
1095
|
}
|
|
834
1096
|
return errors;
|
|
835
1097
|
};
|
|
1098
|
+
|
|
1099
|
+
export const dockerSecretResourceNameDomain = 'serve.zone/docker-secret-resource/v1' as const;
|
|
1100
|
+
export const workloadInitEnvironmentMapPath = '/run/serve.zone/workloadinit-map-v1.json' as const;
|
|
1101
|
+
export const workloadRuntimeAssetRoot = '/opt/serve.zone/runtime-assets' as const;
|
|
1102
|
+
export const workloadInitExecutablePath =
|
|
1103
|
+
'/opt/serve.zone/runtime-assets/workloadinit/v1/workloadinit' as const;
|
|
1104
|
+
export const workloadInitEnvironmentMapMode = 0o444 as const;
|
|
1105
|
+
export const workloadRuntimeAssetsReadOnly = true as const;
|
|
1106
|
+
|
|
1107
|
+
const encodeLowercaseBase32 = (bytesArg: Uint8Array): string => {
|
|
1108
|
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
|
|
1109
|
+
let output = '';
|
|
1110
|
+
let accumulator = 0;
|
|
1111
|
+
let bitCount = 0;
|
|
1112
|
+
for (const byte of bytesArg) {
|
|
1113
|
+
accumulator = (accumulator << 8) | byte;
|
|
1114
|
+
bitCount += 8;
|
|
1115
|
+
while (bitCount >= 5) {
|
|
1116
|
+
bitCount -= 5;
|
|
1117
|
+
output += alphabet[(accumulator >>> bitCount) & 31];
|
|
1118
|
+
}
|
|
1119
|
+
accumulator &= (1 << bitCount) - 1;
|
|
1120
|
+
}
|
|
1121
|
+
if (bitCount > 0) output += alphabet[(accumulator << (5 - bitCount)) & 31];
|
|
1122
|
+
return output;
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
/** Stable resource name; no logical key or value material appears in Docker. */
|
|
1126
|
+
export const createDockerSecretResourceName = async (
|
|
1127
|
+
secretVersionIdArg: string,
|
|
1128
|
+
): Promise<`szsv-${string}`> => {
|
|
1129
|
+
if (!isSecretIdentifier(secretVersionIdArg)) {
|
|
1130
|
+
throw new Error('secretVersionId must be a canonical identifier');
|
|
1131
|
+
}
|
|
1132
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
1133
|
+
'SHA-256',
|
|
1134
|
+
new TextEncoder().encode(`${dockerSecretResourceNameDomain}\0${secretVersionIdArg}`),
|
|
1135
|
+
));
|
|
1136
|
+
const encoded = encodeLowercaseBase32(digest);
|
|
1137
|
+
if (encoded.length !== 52) throw new Error('unexpected SHA-256 base32 length');
|
|
1138
|
+
return `szsv-${encoded}`;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
export const createLauncherSecretSourcePath = (
|
|
1142
|
+
resourceArg: string,
|
|
1143
|
+
): `/run/serve.zone/secrets/${string}` => {
|
|
1144
|
+
if (!/^szsv-[a-z2-7]{52}$/.test(resourceArg)) {
|
|
1145
|
+
throw new Error('resource must be a canonical serve.zone secret resource name');
|
|
1146
|
+
}
|
|
1147
|
+
return `/run/serve.zone/secrets/${resourceArg}`;
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
export interface IWorkloadInitEnvironmentMapEntryV1 {
|
|
1151
|
+
variableName: string;
|
|
1152
|
+
resource: string;
|
|
1153
|
+
source: string;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
export interface IWorkloadInitEnvironmentMapV1 {
|
|
1157
|
+
schemaVersion: 1;
|
|
1158
|
+
manifestId: string;
|
|
1159
|
+
manifestDigest: TSha256Digest;
|
|
1160
|
+
entries: IWorkloadInitEnvironmentMapEntryV1[];
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* The nonsecret map is generated only from launcher deliveries. Direct file
|
|
1165
|
+
* deliveries mount their version resource at the declared target without the
|
|
1166
|
+
* launcher. Runtime assets are mounted read-only; no shell, symlink, or
|
|
1167
|
+
* aggregate material file participates in delivery.
|
|
1168
|
+
*/
|
|
1169
|
+
export const createWorkloadInitEnvironmentMap = async (
|
|
1170
|
+
manifestArg: IResolvedSecretManifest,
|
|
1171
|
+
): Promise<IWorkloadInitEnvironmentMapV1> => {
|
|
1172
|
+
if (validateResolvedSecretManifest(manifestArg).length > 0
|
|
1173
|
+
|| !await verifyResolvedSecretManifestDigest(manifestArg)) {
|
|
1174
|
+
throw new Error('workload init map requires a valid digest-verified manifest');
|
|
1175
|
+
}
|
|
1176
|
+
const entries = await Promise.all(manifestArg.entries
|
|
1177
|
+
.filter((entryArg) => entryArg.delivery.type === 'launcher-environment')
|
|
1178
|
+
.map(async (entryArg) => {
|
|
1179
|
+
const resource = await createDockerSecretResourceName(entryArg.secretVersionId);
|
|
1180
|
+
return {
|
|
1181
|
+
variableName: (entryArg.delivery as ISecretLauncherEnvironmentDelivery).variableName,
|
|
1182
|
+
resource,
|
|
1183
|
+
source: createLauncherSecretSourcePath(resource),
|
|
1184
|
+
};
|
|
1185
|
+
}));
|
|
1186
|
+
entries.sort((leftArg, rightArg) => (
|
|
1187
|
+
leftArg.variableName < rightArg.variableName
|
|
1188
|
+
? -1
|
|
1189
|
+
: leftArg.variableName > rightArg.variableName ? 1 : 0
|
|
1190
|
+
));
|
|
1191
|
+
if (new Set(entries.map((entryArg) => entryArg.variableName)).size !== entries.length
|
|
1192
|
+
|| new Set(entries.map((entryArg) => entryArg.resource)).size !== entries.length
|
|
1193
|
+
|| new Set(entries.map((entryArg) => entryArg.source)).size !== entries.length) {
|
|
1194
|
+
throw new Error('workload init map entries must have unique variable, resource, and source fields');
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
schemaVersion: 1,
|
|
1198
|
+
manifestId: manifestArg.id,
|
|
1199
|
+
manifestDigest: manifestArg.digest,
|
|
1200
|
+
entries,
|
|
1201
|
+
};
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
export const verifyWorkloadInitEnvironmentMap = async (
|
|
1205
|
+
mapArg: unknown,
|
|
1206
|
+
manifestArg: IResolvedSecretManifest,
|
|
1207
|
+
): Promise<boolean> => {
|
|
1208
|
+
if (!isRecord(mapArg)
|
|
1209
|
+
|| !hasExactKeys(mapArg, ['schemaVersion', 'manifestId', 'manifestDigest', 'entries'])) {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
const expected = await createWorkloadInitEnvironmentMap(manifestArg);
|
|
1214
|
+
return JSON.stringify(mapArg) === JSON.stringify(expected);
|
|
1215
|
+
} catch {
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
export interface IWorkloadInitWrappedInvocation {
|
|
1221
|
+
command: [typeof workloadInitExecutablePath];
|
|
1222
|
+
args: string[];
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
export const createWorkloadInitWrappedInvocation = (
|
|
1226
|
+
originalArgvArg: string[],
|
|
1227
|
+
): IWorkloadInitWrappedInvocation => {
|
|
1228
|
+
if (!Array.isArray(originalArgvArg)
|
|
1229
|
+
|| originalArgvArg.length === 0
|
|
1230
|
+
|| originalArgvArg.some((argumentArg) => typeof argumentArg !== 'string'
|
|
1231
|
+
|| argumentArg.length === 0
|
|
1232
|
+
|| argumentArg.includes('\0'))) {
|
|
1233
|
+
throw new Error('original argv must be a non-empty canonical argument vector');
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
command: [workloadInitExecutablePath],
|
|
1237
|
+
args: ['run', '--map', workloadInitEnvironmentMapPath, '--', ...originalArgvArg],
|
|
1238
|
+
};
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* A Docker secret resource and its nonsecret map remain while any desired,
|
|
1243
|
+
* applied, or previous-accepted retained manifest references them. Deletion is
|
|
1244
|
+
* allowed only after no retained reference exists and Docker confirms the
|
|
1245
|
+
* corresponding rollout or service removal.
|
|
1246
|
+
*/
|
|
1247
|
+
export interface ISecretRuntimeResourceRetention {
|
|
1248
|
+
resource: string;
|
|
1249
|
+
retainedManifestIds: string[];
|
|
1250
|
+
dockerRemovalConfirmed: boolean;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export type TSecretRecipientLifecycleState = 'active' | 'retiring' | 'revoked';
|
|
1254
|
+
|
|
1255
|
+
interface ISecretRecipientMetadataBase {
|
|
1256
|
+
schemaVersion: 1;
|
|
1257
|
+
recipientKeyId: string;
|
|
1258
|
+
/** Canonical unpadded base64url raw 32-byte X25519 public key. */
|
|
1259
|
+
publicKey: string;
|
|
1260
|
+
generation: number;
|
|
1261
|
+
activatedAt: number;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export interface IActiveSecretRecipientMetadata extends ISecretRecipientMetadataBase {
|
|
1265
|
+
lifecycleState: 'active';
|
|
1266
|
+
acceptUntil?: never;
|
|
1267
|
+
revokedAt?: never;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
export interface IRetiringSecretRecipientMetadata extends ISecretRecipientMetadataBase {
|
|
1271
|
+
lifecycleState: 'retiring';
|
|
1272
|
+
acceptUntil: number;
|
|
1273
|
+
revokedAt?: never;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
export interface IRevokedSecretRecipientMetadata extends ISecretRecipientMetadataBase {
|
|
1277
|
+
lifecycleState: 'revoked';
|
|
1278
|
+
acceptUntil?: never;
|
|
1279
|
+
revokedAt: number;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
export type TSecretRecipientMetadata =
|
|
1283
|
+
| IActiveSecretRecipientMetadata
|
|
1284
|
+
| IRetiringSecretRecipientMetadata
|
|
1285
|
+
| IRevokedSecretRecipientMetadata;
|
|
1286
|
+
|
|
1287
|
+
export type TSecretIngressRecipient = TSecretRecipientMetadata;
|
|
1288
|
+
|
|
1289
|
+
export const secretRecipientLimits = Object.freeze({
|
|
1290
|
+
maximumRecipients: 64,
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
const rawX25519PublicKeyRegex = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
1294
|
+
|
|
1295
|
+
export const validateSecretRecipientMetadata = (
|
|
1296
|
+
recipientArg: unknown,
|
|
1297
|
+
): string[] => {
|
|
1298
|
+
if (!isRecord(recipientArg)) return ['secret recipient must be an object'];
|
|
1299
|
+
const lifecycleState = recipientArg.lifecycleState;
|
|
1300
|
+
const requiredKeys = lifecycleState === 'active'
|
|
1301
|
+
? ['schemaVersion', 'recipientKeyId', 'publicKey', 'generation', 'activatedAt', 'lifecycleState']
|
|
1302
|
+
: lifecycleState === 'retiring'
|
|
1303
|
+
? ['schemaVersion', 'recipientKeyId', 'publicKey', 'generation', 'activatedAt', 'lifecycleState', 'acceptUntil']
|
|
1304
|
+
: lifecycleState === 'revoked'
|
|
1305
|
+
? ['schemaVersion', 'recipientKeyId', 'publicKey', 'generation', 'activatedAt', 'lifecycleState', 'revokedAt']
|
|
1306
|
+
: [];
|
|
1307
|
+
if (requiredKeys.length === 0 || !hasExactKeys(recipientArg, requiredKeys)) {
|
|
1308
|
+
return ['secret recipient must use its exact lifecycle schema'];
|
|
1309
|
+
}
|
|
1310
|
+
const errors: string[] = [];
|
|
1311
|
+
if (recipientArg.schemaVersion !== 1) errors.push('secret recipient schemaVersion must be 1');
|
|
1312
|
+
if (!isSecretIdentifier(recipientArg.recipientKeyId)) {
|
|
1313
|
+
errors.push('secret recipient key id must be canonical');
|
|
1314
|
+
}
|
|
1315
|
+
if (typeof recipientArg.publicKey !== 'string'
|
|
1316
|
+
|| !rawX25519PublicKeyRegex.test(recipientArg.publicKey)) {
|
|
1317
|
+
errors.push('secret recipient public key must be canonical base64url raw X25519 material');
|
|
1318
|
+
}
|
|
1319
|
+
for (const field of ['generation', 'activatedAt'] as const) {
|
|
1320
|
+
if (!Number.isSafeInteger(recipientArg[field]) || (recipientArg[field] as number) < 1) {
|
|
1321
|
+
errors.push(`secret recipient ${field} must be a positive integer`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (lifecycleState === 'retiring'
|
|
1325
|
+
&& (!Number.isSafeInteger(recipientArg.acceptUntil)
|
|
1326
|
+
|| (recipientArg.acceptUntil as number) <= (recipientArg.activatedAt as number))) {
|
|
1327
|
+
errors.push('retiring secret recipient acceptUntil must follow activation');
|
|
1328
|
+
}
|
|
1329
|
+
if (lifecycleState === 'revoked'
|
|
1330
|
+
&& (!Number.isSafeInteger(recipientArg.revokedAt)
|
|
1331
|
+
|| (recipientArg.revokedAt as number) < (recipientArg.activatedAt as number))) {
|
|
1332
|
+
errors.push('revoked secret recipient revokedAt must not precede activation');
|
|
1333
|
+
}
|
|
1334
|
+
return errors;
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
export const validateSecretRecipientSet = (
|
|
1338
|
+
recipientsArg: unknown,
|
|
1339
|
+
): string[] => {
|
|
1340
|
+
if (!Array.isArray(recipientsArg)
|
|
1341
|
+
|| recipientsArg.length === 0
|
|
1342
|
+
|| recipientsArg.length > secretRecipientLimits.maximumRecipients) {
|
|
1343
|
+
return [
|
|
1344
|
+
`secret recipient set must contain between 1 and ${secretRecipientLimits.maximumRecipients} entries`,
|
|
1345
|
+
];
|
|
1346
|
+
}
|
|
1347
|
+
const errors = recipientsArg.flatMap((recipientArg, indexArg) => (
|
|
1348
|
+
validateSecretRecipientMetadata(recipientArg)
|
|
1349
|
+
.map((errorArg) => `recipients[${indexArg}].${errorArg}`)
|
|
1350
|
+
));
|
|
1351
|
+
const recipients = recipientsArg.filter(isRecord);
|
|
1352
|
+
if (recipients.filter((recipientArg) => recipientArg.lifecycleState === 'active').length !== 1) {
|
|
1353
|
+
errors.push('secret recipient set must contain exactly one active recipient');
|
|
1354
|
+
}
|
|
1355
|
+
const keyIds = recipients.map((recipientArg) => recipientArg.recipientKeyId);
|
|
1356
|
+
const generations = recipients.map((recipientArg) => recipientArg.generation);
|
|
1357
|
+
if (new Set(keyIds).size !== keyIds.length) errors.push('secret recipient key ids must be unique');
|
|
1358
|
+
if (new Set(generations).size !== generations.length) {
|
|
1359
|
+
errors.push('secret recipient generations must be unique');
|
|
1360
|
+
}
|
|
1361
|
+
const activeRecipient = recipients.find((recipientArg) => recipientArg.lifecycleState === 'active');
|
|
1362
|
+
const numericGenerations = generations.filter((generationArg): generationArg is number => (
|
|
1363
|
+
Number.isSafeInteger(generationArg)
|
|
1364
|
+
));
|
|
1365
|
+
if (activeRecipient
|
|
1366
|
+
&& Number.isSafeInteger(activeRecipient.generation)
|
|
1367
|
+
&& numericGenerations.length > 0
|
|
1368
|
+
&& activeRecipient.generation !== Math.max(...numericGenerations)) {
|
|
1369
|
+
errors.push('the active secret recipient must have the greatest generation');
|
|
1370
|
+
}
|
|
1371
|
+
return errors;
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
/** New envelopes target only active recipients. */
|
|
1375
|
+
export const canIssueNewSecretEnvelope = (
|
|
1376
|
+
recipientArg: TSecretRecipientMetadata,
|
|
1377
|
+
): boolean => recipientArg.lifecycleState === 'active';
|
|
1378
|
+
|
|
1379
|
+
export const validateSecretValueInputForRecipient = async (
|
|
1380
|
+
valueInputArg: unknown,
|
|
1381
|
+
recipientArg: TSecretRecipientMetadata,
|
|
1382
|
+
expectedContextArg: Uint8Array,
|
|
1383
|
+
): Promise<string[]> => {
|
|
1384
|
+
const errors = validateSecretValueInput(valueInputArg);
|
|
1385
|
+
if (errors.length > 0 || !isRecord(valueInputArg) || valueInputArg.mode !== 'sealed') {
|
|
1386
|
+
return errors;
|
|
1387
|
+
}
|
|
1388
|
+
if (!canIssueNewSecretEnvelope(recipientArg)) {
|
|
1389
|
+
errors.push('new sealed inputs require the active ingress recipient');
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(valueInputArg.envelope);
|
|
1393
|
+
if (envelope.recipientKeyId !== recipientArg.recipientKeyId) {
|
|
1394
|
+
errors.push('sealed input recipient does not match the active ingress recipient');
|
|
1395
|
+
}
|
|
1396
|
+
if (!await verifySecretEnvelopeContext(envelope, expectedContextArg)) {
|
|
1397
|
+
errors.push('sealed input context does not match the request');
|
|
1398
|
+
}
|
|
1399
|
+
} catch {
|
|
1400
|
+
// validateSecretValueInput already reports malformed envelopes.
|
|
1401
|
+
}
|
|
1402
|
+
return errors;
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
/** Retiring recipients open only already-issued envelopes before their deadline. */
|
|
1406
|
+
export const canOpenIssuedSecretEnvelope = (
|
|
1407
|
+
recipientArg: TSecretRecipientMetadata,
|
|
1408
|
+
issuedAtArg: number,
|
|
1409
|
+
nowArg: number,
|
|
1410
|
+
): boolean => Number.isSafeInteger(issuedAtArg)
|
|
1411
|
+
&& Number.isSafeInteger(nowArg)
|
|
1412
|
+
&& issuedAtArg >= recipientArg.activatedAt
|
|
1413
|
+
&& issuedAtArg <= nowArg
|
|
1414
|
+
&& (recipientArg.lifecycleState === 'active'
|
|
1415
|
+
|| (recipientArg.lifecycleState === 'retiring'
|
|
1416
|
+
&& issuedAtArg < recipientArg.acceptUntil
|
|
1417
|
+
&& nowArg < recipientArg.acceptUntil));
|
|
1418
|
+
|
|
1419
|
+
const canonicalSecretOwner = (ownerArg: TSecretOwner): Record<string, string> => {
|
|
1420
|
+
switch (ownerArg.kind) {
|
|
1421
|
+
case 'service': return { kind: ownerArg.kind, serviceId: ownerArg.serviceId };
|
|
1422
|
+
case 'secret-set': return { kind: ownerArg.kind, secretSetId: ownerArg.secretSetId };
|
|
1423
|
+
case 'platform-provider': return {
|
|
1424
|
+
kind: ownerArg.kind,
|
|
1425
|
+
providerConfigId: ownerArg.providerConfigId,
|
|
1426
|
+
};
|
|
1427
|
+
case 'system': return { kind: ownerArg.kind, systemId: ownerArg.systemId };
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
const canonicalSecretDelivery = (deliveryArg: TSecretDelivery): Record<string, unknown> => (
|
|
1432
|
+
deliveryArg.type === 'launcher-environment'
|
|
1433
|
+
? {
|
|
1434
|
+
type: deliveryArg.type,
|
|
1435
|
+
variableName: deliveryArg.variableName,
|
|
1436
|
+
uid: deliveryArg.uid,
|
|
1437
|
+
gid: deliveryArg.gid,
|
|
1438
|
+
mode: deliveryArg.mode,
|
|
1439
|
+
}
|
|
1440
|
+
: {
|
|
1441
|
+
type: deliveryArg.type,
|
|
1442
|
+
targetPath: deliveryArg.targetPath,
|
|
1443
|
+
uid: deliveryArg.uid,
|
|
1444
|
+
gid: deliveryArg.gid,
|
|
1445
|
+
mode: deliveryArg.mode,
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
const contextBytes = (valueArg: Record<string, unknown>): Uint8Array => (
|
|
1450
|
+
new TextEncoder().encode(JSON.stringify(valueArg))
|
|
1451
|
+
);
|
|
1452
|
+
|
|
1453
|
+
export const createSecretCreateEnvelopeContext = (contextArg: {
|
|
1454
|
+
mutationId: string;
|
|
1455
|
+
target: TSecretMutationTarget;
|
|
1456
|
+
key: string;
|
|
1457
|
+
environment: TSecretEnvironment;
|
|
1458
|
+
delivery: TSecretDelivery;
|
|
1459
|
+
expectedTargetSecretsRevision: number;
|
|
1460
|
+
}): Uint8Array => contextBytes({
|
|
1461
|
+
schemaVersion: 1,
|
|
1462
|
+
purpose: 'serve.zone/secret-create',
|
|
1463
|
+
mutationId: contextArg.mutationId,
|
|
1464
|
+
target: canonicalSecretOwner(contextArg.target),
|
|
1465
|
+
key: contextArg.key,
|
|
1466
|
+
environment: contextArg.environment,
|
|
1467
|
+
delivery: canonicalSecretDelivery(contextArg.delivery),
|
|
1468
|
+
expectedTargetSecretsRevision: contextArg.expectedTargetSecretsRevision,
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
export const createSecretRotateEnvelopeContext = (contextArg: {
|
|
1472
|
+
mutationId: string;
|
|
1473
|
+
secretId: string;
|
|
1474
|
+
expectedSecretRevision: number;
|
|
1475
|
+
expectedActiveVersionId: string;
|
|
1476
|
+
expectedTargetSecretsRevision: number;
|
|
1477
|
+
}): Uint8Array => contextBytes({
|
|
1478
|
+
schemaVersion: 1,
|
|
1479
|
+
purpose: 'serve.zone/secret-rotate',
|
|
1480
|
+
mutationId: contextArg.mutationId,
|
|
1481
|
+
secretId: contextArg.secretId,
|
|
1482
|
+
expectedSecretRevision: contextArg.expectedSecretRevision,
|
|
1483
|
+
expectedActiveVersionId: contextArg.expectedActiveVersionId,
|
|
1484
|
+
expectedTargetSecretsRevision: contextArg.expectedTargetSecretsRevision,
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
export const createAppStoreInstallSecretEnvelopeContext = (contextArg: {
|
|
1488
|
+
mutationId: string;
|
|
1489
|
+
appId: string;
|
|
1490
|
+
appVersion: string;
|
|
1491
|
+
serviceId: string;
|
|
1492
|
+
key: string;
|
|
1493
|
+
delivery: TSecretDelivery;
|
|
1494
|
+
generation?: { encoding: TSecretGeneratedEncoding; bytes: number };
|
|
1495
|
+
}): Uint8Array => contextBytes({
|
|
1496
|
+
schemaVersion: 1,
|
|
1497
|
+
purpose: 'serve.zone/app-install-secret',
|
|
1498
|
+
mutationId: contextArg.mutationId,
|
|
1499
|
+
appId: contextArg.appId,
|
|
1500
|
+
appVersion: contextArg.appVersion,
|
|
1501
|
+
serviceId: contextArg.serviceId,
|
|
1502
|
+
key: contextArg.key,
|
|
1503
|
+
delivery: canonicalSecretDelivery(contextArg.delivery),
|
|
1504
|
+
generation: contextArg.generation
|
|
1505
|
+
? { encoding: contextArg.generation.encoding, bytes: contextArg.generation.bytes }
|
|
1506
|
+
: null,
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
export const createResolvedSecretMaterialEnvelopeContext = (contextArg: {
|
|
1510
|
+
recipientKeyId: string;
|
|
1511
|
+
organizationId: string;
|
|
1512
|
+
clusterId: string;
|
|
1513
|
+
serviceId: string;
|
|
1514
|
+
imageRolloutId: string;
|
|
1515
|
+
imageRolloutGeneration: number;
|
|
1516
|
+
requestedImageDigest: TSha256Digest;
|
|
1517
|
+
secretRolloutId: string;
|
|
1518
|
+
secretRolloutGeneration: number;
|
|
1519
|
+
manifestId: string;
|
|
1520
|
+
manifestDigest: TSha256Digest;
|
|
1521
|
+
secretId: string;
|
|
1522
|
+
secretVersionId: string;
|
|
1523
|
+
}): Uint8Array => contextBytes({
|
|
1524
|
+
schemaVersion: 1,
|
|
1525
|
+
purpose: 'serve.zone/resolved-secret-material',
|
|
1526
|
+
recipientKeyId: contextArg.recipientKeyId,
|
|
1527
|
+
organizationId: contextArg.organizationId,
|
|
1528
|
+
clusterId: contextArg.clusterId,
|
|
1529
|
+
serviceId: contextArg.serviceId,
|
|
1530
|
+
imageRolloutId: contextArg.imageRolloutId,
|
|
1531
|
+
imageRolloutGeneration: contextArg.imageRolloutGeneration,
|
|
1532
|
+
requestedImageDigest: contextArg.requestedImageDigest,
|
|
1533
|
+
secretRolloutId: contextArg.secretRolloutId,
|
|
1534
|
+
secretRolloutGeneration: contextArg.secretRolloutGeneration,
|
|
1535
|
+
manifestId: contextArg.manifestId,
|
|
1536
|
+
manifestDigest: contextArg.manifestDigest,
|
|
1537
|
+
secretId: contextArg.secretId,
|
|
1538
|
+
secretVersionId: contextArg.secretVersionId,
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
export const createSecretRecipientEnrollmentChallengeContext = (contextArg: {
|
|
1542
|
+
enrollmentId: string;
|
|
1543
|
+
clusterId: string;
|
|
1544
|
+
recipientKeyId: string;
|
|
1545
|
+
publicKey: string;
|
|
1546
|
+
generation: number;
|
|
1547
|
+
expiresAt: number;
|
|
1548
|
+
}): Uint8Array => contextBytes({
|
|
1549
|
+
schemaVersion: 1,
|
|
1550
|
+
purpose: 'serve.zone/secret-recipient-enrollment-challenge',
|
|
1551
|
+
enrollmentId: contextArg.enrollmentId,
|
|
1552
|
+
clusterId: contextArg.clusterId,
|
|
1553
|
+
recipientKeyId: contextArg.recipientKeyId,
|
|
1554
|
+
publicKey: contextArg.publicKey,
|
|
1555
|
+
generation: contextArg.generation,
|
|
1556
|
+
expiresAt: contextArg.expiresAt,
|
|
1557
|
+
});
|