@serve.zone/interfaces 22.0.0 → 23.0.1
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 +19 -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.runtime.d.ts +3 -2
- package/dist_ts/data/coremail.runtime.js +10 -5
- 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/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 +110 -29
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/types.ts +251 -25
- package/ts/data/coremail.runtime.ts +18 -4
- 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/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/00_commitinfo_data.ts
CHANGED
package/ts/appstore/types.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
createAppStoreInstallSecretEnvelopeContext,
|
|
3
|
+
validateSecretDelivery,
|
|
4
|
+
validateSecretValueInput,
|
|
5
|
+
validateSecretValueInputForRecipient,
|
|
6
|
+
type TSecretDelivery,
|
|
7
|
+
type TSecretGeneratedEncoding,
|
|
8
|
+
type TSecretIngressRecipient,
|
|
9
|
+
type TSecretValueInput,
|
|
10
|
+
} from '../data/secret.js';
|
|
2
11
|
import {
|
|
3
12
|
validateHostedAppRoleDefinitions,
|
|
4
13
|
type IHostedAppRoleDefinition,
|
|
@@ -36,10 +45,10 @@ export type TAppStoreTrackingMode = 'tag' | 'digest';
|
|
|
36
45
|
export type TAppStoreUpgradeStrategy = 'semver' | 'branch' | 'dockerDigest';
|
|
37
46
|
|
|
38
47
|
export const appStoreStorageFeatureIds = {
|
|
39
|
-
|
|
48
|
+
bindingsV2: 'storage.bindings.v2',
|
|
40
49
|
filesystemV1: 'storage.filesystem.v1',
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
objectStorageV2: 'storage.object-storage.v2',
|
|
51
|
+
objectStorageFileV2: 'storage.object-storage.file.v2',
|
|
43
52
|
} as const;
|
|
44
53
|
|
|
45
54
|
export type TAppStoreStorageFeatureId =
|
|
@@ -140,21 +149,36 @@ export interface IAppStoreResolvedSource {
|
|
|
140
149
|
resolvedAt: string;
|
|
141
150
|
}
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
interface IAppStoreEnvironmentDeclarationBase {
|
|
144
153
|
key: string;
|
|
145
|
-
value?: string;
|
|
146
154
|
description: string;
|
|
147
155
|
required?: boolean;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface IAppStorePublicEnvironmentDeclaration
|
|
159
|
+
extends IAppStoreEnvironmentDeclarationBase {
|
|
160
|
+
secret?: false;
|
|
161
|
+
/** Public nonsecret default. */
|
|
162
|
+
value?: string;
|
|
163
|
+
delivery?: never;
|
|
164
|
+
generate?: never;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface IAppStoreSecretEnvironmentDeclaration
|
|
168
|
+
extends IAppStoreEnvironmentDeclarationBase {
|
|
169
|
+
secret: true;
|
|
170
|
+
value?: never;
|
|
171
|
+
delivery: TSecretDelivery;
|
|
151
172
|
generate?: {
|
|
152
|
-
|
|
153
|
-
/** Number of random bytes before hexadecimal encoding. */
|
|
173
|
+
encoding: TSecretGeneratedEncoding;
|
|
154
174
|
bytes: number;
|
|
155
175
|
};
|
|
156
176
|
}
|
|
157
177
|
|
|
178
|
+
export type TAppStoreEnvironmentDeclaration =
|
|
179
|
+
| IAppStorePublicEnvironmentDeclaration
|
|
180
|
+
| IAppStoreSecretEnvironmentDeclaration;
|
|
181
|
+
|
|
158
182
|
export interface IAppStoreVolume {
|
|
159
183
|
/** Stable Docker volume name. If omitted, the runtime derives one from service name and mount path. */
|
|
160
184
|
name?: string;
|
|
@@ -251,7 +275,10 @@ export const validateAppStoreVersionPlatformOidc = (
|
|
|
251
275
|
return ['appstore config must be an object'];
|
|
252
276
|
}
|
|
253
277
|
const config = configArg as Record<string, unknown>;
|
|
254
|
-
const errors =
|
|
278
|
+
const errors = [
|
|
279
|
+
...validateAppStorePlatformOidcCapability(config.platformOidc),
|
|
280
|
+
...validateAppStoreEnvironmentDeclarations(config.envVars),
|
|
281
|
+
];
|
|
255
282
|
if (!config.platformOidc || typeof config.platformOidc !== 'object'
|
|
256
283
|
|| Array.isArray(config.platformOidc)) return errors;
|
|
257
284
|
const capability = config.platformOidc as Record<string, unknown>;
|
|
@@ -266,14 +293,7 @@ export const validateAppStoreVersionPlatformOidc = (
|
|
|
266
293
|
: undefined
|
|
267
294
|
)).filter((valueArg): valueArg is string => typeof valueArg === 'string')
|
|
268
295
|
: [];
|
|
269
|
-
const
|
|
270
|
-
? config.secretFiles.map((fileArg) => (
|
|
271
|
-
fileArg && typeof fileArg === 'object' && !Array.isArray(fileArg)
|
|
272
|
-
? (fileArg as Record<string, unknown>).sourceKey
|
|
273
|
-
: undefined
|
|
274
|
-
)).filter((valueArg): valueArg is string => typeof valueArg === 'string')
|
|
275
|
-
: [];
|
|
276
|
-
for (const key of [...ordinaryEnvKeys, ...secretFileKeys]) {
|
|
296
|
+
for (const key of ordinaryEnvKeys) {
|
|
277
297
|
if (oidcKeys.has(key)) {
|
|
278
298
|
errors.push(`platformOidc environment key '${key}' collides with app runtime input`);
|
|
279
299
|
}
|
|
@@ -327,13 +347,11 @@ export interface IAppStoreVersionConfig {
|
|
|
327
347
|
port?: number;
|
|
328
348
|
/** Exact OCI/Docker argument vector appended after the image entrypoint. */
|
|
329
349
|
containerArgs?: string[];
|
|
330
|
-
/** Secret-bundle keys delivered only through Docker's read-only secret tmpfs. */
|
|
331
|
-
secretFiles?: IServiceSecretFile[];
|
|
332
350
|
targetPorts?: IServiceTargetPort[];
|
|
333
351
|
domains?: IServiceDomainRoute[];
|
|
334
352
|
/** Edge/coretraffic public TCP/UDP exposure, distinct from Docker publishedPorts. */
|
|
335
353
|
publicPortMappings?: IServicePublicPortMapping[];
|
|
336
|
-
envVars?:
|
|
354
|
+
envVars?: TAppStoreEnvironmentDeclaration[];
|
|
337
355
|
/**
|
|
338
356
|
* @deprecated Legacy volume syntax. New templates use storageClasses and
|
|
339
357
|
* storageRequests. Resolver normalization must reject legacy physical driver
|
|
@@ -407,17 +425,225 @@ export interface IServezoneAppStoreManifest {
|
|
|
407
425
|
}
|
|
408
426
|
|
|
409
427
|
export interface IAppStoreInstallRequest {
|
|
428
|
+
mutationId: string;
|
|
410
429
|
appId: string;
|
|
411
|
-
version
|
|
430
|
+
version: string;
|
|
431
|
+
serviceId: string;
|
|
412
432
|
serviceName: string;
|
|
413
433
|
domain?: string;
|
|
414
434
|
/** Legacy install-time override for the web target port. */
|
|
415
435
|
port?: number;
|
|
416
436
|
publishedPorts?: IAppStorePublishedPort[];
|
|
417
437
|
publicPortMappings?: IServicePublicPortMapping[];
|
|
418
|
-
|
|
438
|
+
publicEnvironment: Record<string, string>;
|
|
439
|
+
secretInputs: Array<{ key: string; valueInput: TSecretValueInput }>;
|
|
419
440
|
}
|
|
420
441
|
|
|
442
|
+
const appStoreMutationIdRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
|
|
443
|
+
|
|
444
|
+
export const validateAppStoreEnvironmentDeclarations = (
|
|
445
|
+
declarationsArg: unknown,
|
|
446
|
+
): string[] => {
|
|
447
|
+
if (declarationsArg === undefined) return [];
|
|
448
|
+
if (!Array.isArray(declarationsArg) || declarationsArg.length > 256) {
|
|
449
|
+
return ['envVars must be a bounded array'];
|
|
450
|
+
}
|
|
451
|
+
const errors: string[] = [];
|
|
452
|
+
const keys: string[] = [];
|
|
453
|
+
declarationsArg.forEach((declarationArg, indexArg) => {
|
|
454
|
+
if (!declarationArg || typeof declarationArg !== 'object' || Array.isArray(declarationArg)) {
|
|
455
|
+
errors.push(`envVars[${indexArg}] must be an object`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const declaration = declarationArg as Record<string, unknown>;
|
|
459
|
+
const allowedKeys = declaration.secret === true
|
|
460
|
+
? new Set(['key', 'description', 'required', 'secret', 'delivery', 'generate'])
|
|
461
|
+
: new Set(['key', 'description', 'required', 'secret', 'value']);
|
|
462
|
+
if (Object.keys(declaration).some((keyArg) => !allowedKeys.has(keyArg))) {
|
|
463
|
+
errors.push(`envVars[${indexArg}] contains fields outside its exact channel schema`);
|
|
464
|
+
}
|
|
465
|
+
if (typeof declaration.key !== 'string' || !appStoreEnvKeyRegex.test(declaration.key)) {
|
|
466
|
+
errors.push(`envVars[${indexArg}].key must be canonical`);
|
|
467
|
+
} else {
|
|
468
|
+
keys.push(declaration.key);
|
|
469
|
+
}
|
|
470
|
+
if (typeof declaration.description !== 'string' || declaration.description.length > 1000) {
|
|
471
|
+
errors.push(`envVars[${indexArg}].description must be bounded`);
|
|
472
|
+
}
|
|
473
|
+
if (declaration.required !== undefined && typeof declaration.required !== 'boolean') {
|
|
474
|
+
errors.push(`envVars[${indexArg}].required must be boolean`);
|
|
475
|
+
}
|
|
476
|
+
if (declaration.secret === true) {
|
|
477
|
+
if ('value' in declaration) errors.push(`envVars[${indexArg}] secret declaration forbids value`);
|
|
478
|
+
if (!('delivery' in declaration)) {
|
|
479
|
+
errors.push(`envVars[${indexArg}] secret declaration requires delivery`);
|
|
480
|
+
} else {
|
|
481
|
+
errors.push(...validateSecretDelivery(declaration.delivery)
|
|
482
|
+
.map((errorArg) => `envVars[${indexArg}].${errorArg}`));
|
|
483
|
+
}
|
|
484
|
+
if (declaration.generate !== undefined) {
|
|
485
|
+
errors.push(...validateSecretValueInput({
|
|
486
|
+
mode: 'generated',
|
|
487
|
+
...(declaration.generate as Record<string, unknown>),
|
|
488
|
+
}).map((errorArg) => `envVars[${indexArg}].generate ${errorArg}`));
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
if (declaration.secret !== undefined && declaration.secret !== false) {
|
|
492
|
+
errors.push(`envVars[${indexArg}].secret must be true or false`);
|
|
493
|
+
}
|
|
494
|
+
if ('delivery' in declaration) errors.push(`envVars[${indexArg}] public declaration forbids delivery`);
|
|
495
|
+
if ('generate' in declaration) errors.push(`envVars[${indexArg}] public declaration forbids generate`);
|
|
496
|
+
if (declaration.value !== undefined && typeof declaration.value !== 'string') {
|
|
497
|
+
errors.push(`envVars[${indexArg}].value must be a public string default`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
if (new Set(keys).size !== keys.length) errors.push('envVars keys must be unique');
|
|
502
|
+
return errors;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export const validateAppStoreInstallRequest = async (
|
|
506
|
+
requestArg: unknown,
|
|
507
|
+
configArg: IAppStoreVersionConfig,
|
|
508
|
+
activeIngressRecipientArg: TSecretIngressRecipient,
|
|
509
|
+
): Promise<string[]> => {
|
|
510
|
+
if (!requestArg || typeof requestArg !== 'object' || Array.isArray(requestArg)) {
|
|
511
|
+
return ['install request must be an object'];
|
|
512
|
+
}
|
|
513
|
+
const request = requestArg as Record<string, unknown>;
|
|
514
|
+
const rawDeclarations = configArg && typeof configArg === 'object'
|
|
515
|
+
? configArg.envVars
|
|
516
|
+
: undefined;
|
|
517
|
+
const errors = validateAppStoreEnvironmentDeclarations(rawDeclarations);
|
|
518
|
+
const allowedRequestKeys = new Set([
|
|
519
|
+
'mutationId',
|
|
520
|
+
'appId',
|
|
521
|
+
'version',
|
|
522
|
+
'serviceId',
|
|
523
|
+
'serviceName',
|
|
524
|
+
'domain',
|
|
525
|
+
'port',
|
|
526
|
+
'publishedPorts',
|
|
527
|
+
'publicPortMappings',
|
|
528
|
+
'publicEnvironment',
|
|
529
|
+
'secretInputs',
|
|
530
|
+
]);
|
|
531
|
+
if (Object.keys(request).some((keyArg) => !allowedRequestKeys.has(keyArg))) {
|
|
532
|
+
errors.push('install request contains fields outside its schema');
|
|
533
|
+
}
|
|
534
|
+
if (typeof request.mutationId !== 'string'
|
|
535
|
+
|| !appStoreMutationIdRegex.test(request.mutationId)) {
|
|
536
|
+
errors.push('install mutationId must be canonical');
|
|
537
|
+
}
|
|
538
|
+
for (const field of ['appId', 'version', 'serviceId', 'serviceName'] as const) {
|
|
539
|
+
if (typeof request[field] !== 'string' || !(request[field] as string).trim()) {
|
|
540
|
+
errors.push(`install ${field} must be non-empty`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const publicEnvironment = request.publicEnvironment;
|
|
544
|
+
if (!publicEnvironment || typeof publicEnvironment !== 'object'
|
|
545
|
+
|| Array.isArray(publicEnvironment)
|
|
546
|
+
|| Object.entries(publicEnvironment).some(([keyArg, valueArg]) => (
|
|
547
|
+
!appStoreEnvKeyRegex.test(keyArg) || typeof valueArg !== 'string'
|
|
548
|
+
))) {
|
|
549
|
+
errors.push('install publicEnvironment must be a canonical string map');
|
|
550
|
+
}
|
|
551
|
+
const secretInputs = Array.isArray(request.secretInputs) ? request.secretInputs : [];
|
|
552
|
+
if (!Array.isArray(request.secretInputs) || secretInputs.length > 256) {
|
|
553
|
+
errors.push('install secretInputs must be a bounded array');
|
|
554
|
+
}
|
|
555
|
+
const inputKeys: string[] = [];
|
|
556
|
+
const inputsByKey = new Map<string, Record<string, unknown>>();
|
|
557
|
+
const declarations = Array.isArray(rawDeclarations) && rawDeclarations.length <= 256
|
|
558
|
+
? rawDeclarations.filter((entryArg): entryArg is TAppStoreEnvironmentDeclaration => (
|
|
559
|
+
validateAppStoreEnvironmentDeclarations([entryArg]).length === 0
|
|
560
|
+
))
|
|
561
|
+
: [];
|
|
562
|
+
const publicDeclarations = new Map(declarations
|
|
563
|
+
.filter((entryArg): entryArg is IAppStorePublicEnvironmentDeclaration => entryArg.secret !== true)
|
|
564
|
+
.map((entryArg) => [entryArg.key, entryArg]));
|
|
565
|
+
const secretDeclarations = new Map(declarations
|
|
566
|
+
.filter((entryArg): entryArg is IAppStoreSecretEnvironmentDeclaration => entryArg.secret === true)
|
|
567
|
+
.map((entryArg) => [entryArg.key, entryArg]));
|
|
568
|
+
for (const [indexArg, inputArg] of secretInputs.entries()) {
|
|
569
|
+
if (!inputArg || typeof inputArg !== 'object' || Array.isArray(inputArg)) {
|
|
570
|
+
errors.push(`install secretInputs[${indexArg}] must be an object`);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const input = inputArg as Record<string, unknown>;
|
|
574
|
+
if (JSON.stringify(Object.keys(input).sort()) !== JSON.stringify(['key', 'valueInput'])) {
|
|
575
|
+
errors.push(`install secretInputs[${indexArg}] must use its exact schema`);
|
|
576
|
+
}
|
|
577
|
+
if (typeof input.key !== 'string' || !appStoreEnvKeyRegex.test(input.key)) {
|
|
578
|
+
errors.push(`install secretInputs[${indexArg}].key must be canonical`);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
inputKeys.push(input.key);
|
|
582
|
+
inputsByKey.set(input.key, input);
|
|
583
|
+
const declaration = secretDeclarations.get(input.key);
|
|
584
|
+
const canBuildContext = declaration
|
|
585
|
+
&& typeof request.mutationId === 'string'
|
|
586
|
+
&& appStoreMutationIdRegex.test(request.mutationId)
|
|
587
|
+
&& typeof request.appId === 'string'
|
|
588
|
+
&& Boolean(request.appId.trim())
|
|
589
|
+
&& typeof request.version === 'string'
|
|
590
|
+
&& Boolean(request.version.trim())
|
|
591
|
+
&& typeof request.serviceId === 'string'
|
|
592
|
+
&& Boolean(request.serviceId.trim());
|
|
593
|
+
const expectedContext = canBuildContext
|
|
594
|
+
? createAppStoreInstallSecretEnvelopeContext({
|
|
595
|
+
mutationId: request.mutationId as string,
|
|
596
|
+
appId: request.appId as string,
|
|
597
|
+
appVersion: request.version as string,
|
|
598
|
+
serviceId: request.serviceId as string,
|
|
599
|
+
key: input.key,
|
|
600
|
+
delivery: declaration.delivery,
|
|
601
|
+
generation: declaration.generate,
|
|
602
|
+
})
|
|
603
|
+
: new Uint8Array();
|
|
604
|
+
errors.push(...(await validateSecretValueInputForRecipient(
|
|
605
|
+
input.valueInput,
|
|
606
|
+
activeIngressRecipientArg,
|
|
607
|
+
expectedContext,
|
|
608
|
+
))
|
|
609
|
+
.map((errorArg) => `install secretInputs[${indexArg}].${errorArg}`));
|
|
610
|
+
}
|
|
611
|
+
if (new Set(inputKeys).size !== inputKeys.length) {
|
|
612
|
+
errors.push('install secretInputs keys must be unique');
|
|
613
|
+
}
|
|
614
|
+
for (const key of Object.keys((publicEnvironment || {}) as Record<string, unknown>)) {
|
|
615
|
+
if (!publicDeclarations.has(key)) errors.push(`install public key '${key}' is not declared public`);
|
|
616
|
+
if (inputsByKey.has(key)) errors.push(`install key '${key}' appears in both channels`);
|
|
617
|
+
}
|
|
618
|
+
for (const [key, input] of inputsByKey) {
|
|
619
|
+
const declaration = secretDeclarations.get(key);
|
|
620
|
+
if (!declaration) {
|
|
621
|
+
errors.push(`install secret key '${key}' is not declared secret`);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const valueInput = input.valueInput as TSecretValueInput;
|
|
625
|
+
if (valueInput?.mode === 'generated'
|
|
626
|
+
&& (!declaration.generate
|
|
627
|
+
|| declaration.generate.encoding !== valueInput.encoding
|
|
628
|
+
|| declaration.generate.bytes !== valueInput.bytes)) {
|
|
629
|
+
errors.push(`install secret key '${key}' generation does not match its declaration`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
for (const declaration of publicDeclarations.values()) {
|
|
633
|
+
if (declaration.required
|
|
634
|
+
&& declaration.value === undefined
|
|
635
|
+
&& !Object.hasOwn((publicEnvironment || {}) as object, declaration.key)) {
|
|
636
|
+
errors.push(`install required public key '${declaration.key}' is missing`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const declaration of secretDeclarations.values()) {
|
|
640
|
+
if (declaration.required && !declaration.generate && !inputsByKey.has(declaration.key)) {
|
|
641
|
+
errors.push(`install required secret key '${declaration.key}' is missing`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return errors;
|
|
645
|
+
};
|
|
646
|
+
|
|
421
647
|
export interface IUpgradeableAppStoreService {
|
|
422
648
|
serviceId?: string;
|
|
423
649
|
serviceName: string;
|
|
@@ -80,16 +80,25 @@ export const coreMailContractLimits = Object.freeze({
|
|
|
80
80
|
} as const);
|
|
81
81
|
|
|
82
82
|
export class CoreMailContractError extends Error {
|
|
83
|
-
public
|
|
83
|
+
public readonly code: TCoreMailErrorCode;
|
|
84
|
+
|
|
85
|
+
public constructor(
|
|
86
|
+
reasonArg: string,
|
|
87
|
+
codeArg: TCoreMailErrorCode = 'INVALID_REQUEST',
|
|
88
|
+
) {
|
|
84
89
|
super(`CoreMail contract error: ${reasonArg}`);
|
|
85
90
|
this.name = 'CoreMailContractError';
|
|
91
|
+
this.code = codeArg;
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
type TStrictRecord = Record<string, unknown>;
|
|
90
96
|
|
|
91
|
-
const fail = (
|
|
92
|
-
|
|
97
|
+
const fail = (
|
|
98
|
+
reasonArg: string,
|
|
99
|
+
codeArg: TCoreMailErrorCode = 'INVALID_REQUEST',
|
|
100
|
+
): never => {
|
|
101
|
+
throw new CoreMailContractError(reasonArg, codeArg);
|
|
93
102
|
};
|
|
94
103
|
|
|
95
104
|
const compareStrings = (leftArg: string, rightArg: string): number =>
|
|
@@ -637,9 +646,14 @@ const normalizeContentDescriptor = (
|
|
|
637
646
|
record.lengthBytes,
|
|
638
647
|
`${pathArg}.lengthBytes`,
|
|
639
648
|
0,
|
|
640
|
-
maximumBytes,
|
|
641
649
|
),
|
|
642
650
|
};
|
|
651
|
+
if (normalized.lengthBytes > maximumBytes) {
|
|
652
|
+
fail(
|
|
653
|
+
`${pathArg}.lengthBytes exceeds the ${kind} content budget of ${maximumBytes}`,
|
|
654
|
+
'PAYLOAD_LIMIT_EXCEEDED',
|
|
655
|
+
);
|
|
656
|
+
}
|
|
643
657
|
if (Object.hasOwn(record, 'filename')) {
|
|
644
658
|
normalized.filename = requireDisplayText(
|
|
645
659
|
record.filename,
|
|
@@ -3,7 +3,7 @@ import type { IGatewayRouteDnsResult } from './gateway.js';
|
|
|
3
3
|
import type { IImageRolloutStatus, IImmutableImageDeploymentPlan, TSha256Digest } from './immutableimage.js';
|
|
4
4
|
import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
|
|
5
5
|
import type { IRegistryTarget } from './registry.js';
|
|
6
|
-
import { normalizeServiceAbsolutePath, validateServiceContainerArgs
|
|
6
|
+
import { normalizeServiceAbsolutePath, validateServiceContainerArgs } from './service.js';
|
|
7
7
|
import type { TServiceTargetPortRef } from './serviceports.js';
|
|
8
8
|
|
|
9
9
|
export type TDeploymentOperationMode = 'existing-service' | 'greenfield';
|
|
@@ -162,10 +162,12 @@ export const validateDeploymentProposedConfiguration = (
|
|
|
162
162
|
|| configurationArg.projectName.length > 200) {
|
|
163
163
|
errors.push('projectName must be bounded and non-empty');
|
|
164
164
|
}
|
|
165
|
-
if (!validateStringArray(configurationArg.targetPlatforms,
|
|
166
|
-
|
|
167
|
-
)))
|
|
168
|
-
|
|
165
|
+
if (!validateStringArray(configurationArg.targetPlatforms, 2, (platformArg) => (
|
|
166
|
+
platformArg === 'linux/amd64' || platformArg === 'linux/arm64'
|
|
167
|
+
)) || (configurationArg.targetPlatforms as string[]).length === 0
|
|
168
|
+
|| JSON.stringify(configurationArg.targetPlatforms)
|
|
169
|
+
!== JSON.stringify([...(configurationArg.targetPlatforms as string[])].sort())) {
|
|
170
|
+
errors.push('targetPlatforms must be a sorted non-empty supported platform set');
|
|
169
171
|
}
|
|
170
172
|
if (!validateStringArray(configurationArg.publicDomains, 32, (domainArg) => (
|
|
171
173
|
canonicalizeDeploymentHostname(domainArg) === domainArg
|
|
@@ -177,33 +179,7 @@ export const validateDeploymentProposedConfiguration = (
|
|
|
177
179
|
))) {
|
|
178
180
|
errors.push('environmentVariableNames must be unique canonical names');
|
|
179
181
|
}
|
|
180
|
-
if (!validateStringArray(configurationArg.secretReferenceIds, 64, (idArg) => (
|
|
181
|
-
idArg.length <= 253 && /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(idArg)
|
|
182
|
-
))) {
|
|
183
|
-
errors.push('secretReferenceIds must be unique canonical identifiers');
|
|
184
|
-
}
|
|
185
|
-
const volumeMountPaths = Array.isArray(configurationArg.volumeMounts)
|
|
186
|
-
? configurationArg.volumeMounts.flatMap((mountArg) => (
|
|
187
|
-
isRecord(mountArg) && typeof mountArg.mountPath === 'string' ? [mountArg.mountPath] : []
|
|
188
|
-
))
|
|
189
|
-
: [];
|
|
190
182
|
errors.push(...validateServiceContainerArgs(configurationArg.containerArgs));
|
|
191
|
-
errors.push(...validateServiceSecretFiles(configurationArg.secretFiles, volumeMountPaths));
|
|
192
|
-
if (Array.isArray(configurationArg.secretFiles)
|
|
193
|
-
&& Array.isArray(configurationArg.environmentVariableNames)) {
|
|
194
|
-
const publicEnvironmentNames = new Set(
|
|
195
|
-
configurationArg.environmentVariableNames.filter((nameArg): nameArg is string => (
|
|
196
|
-
typeof nameArg === 'string'
|
|
197
|
-
)),
|
|
198
|
-
);
|
|
199
|
-
if (configurationArg.secretFiles.some((fileArg) => (
|
|
200
|
-
isRecord(fileArg)
|
|
201
|
-
&& typeof fileArg.sourceKey === 'string'
|
|
202
|
-
&& publicEnvironmentNames.has(fileArg.sourceKey)
|
|
203
|
-
))) {
|
|
204
|
-
errors.push('secretFiles source keys must not be declared as environment variables');
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
183
|
if (!Array.isArray(configurationArg.containerPorts)
|
|
208
184
|
|| configurationArg.containerPorts.length > 64
|
|
209
185
|
|| configurationArg.containerPorts.some((portArg) => !Number.isSafeInteger(portArg)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
TImmutableContainerPlatform,
|
|
2
3
|
TOciManifestMediaType,
|
|
3
4
|
TSha256Digest,
|
|
4
5
|
} from './immutableimage.js';
|
|
5
6
|
import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
|
|
6
7
|
import type { TServiceDeploymentCapability } from './user.js';
|
|
7
|
-
import type { IServiceSecretFile } from './service.js';
|
|
8
8
|
import type {
|
|
9
9
|
IDeploymentRouteRequest,
|
|
10
10
|
IServiceDeploymentOperation,
|
|
@@ -145,14 +145,11 @@ export interface IDeploymentPreflightProposedConfiguration {
|
|
|
145
145
|
schemaVersion: 1;
|
|
146
146
|
declarationComplete: boolean;
|
|
147
147
|
projectName: string;
|
|
148
|
-
targetPlatforms:
|
|
148
|
+
targetPlatforms: TImmutableContainerPlatform[];
|
|
149
149
|
publicDomains: string[];
|
|
150
150
|
environmentVariableNames: string[];
|
|
151
|
-
secretReferenceIds: string[];
|
|
152
151
|
/** Exact OCI/Docker argument vector appended after the image entrypoint. */
|
|
153
152
|
containerArgs?: string[];
|
|
154
|
-
/** Secret-bundle keys that must be delivered only as Docker secret files. */
|
|
155
|
-
secretFiles?: IServiceSecretFile[];
|
|
156
153
|
containerPorts: number[];
|
|
157
154
|
volumeMounts: IDeploymentPreflightProposedVolumeMount[];
|
|
158
155
|
requiredCapabilities: Array<'database' | 'objectstorage' | 'pushnotification'>;
|
package/ts/data/hostedapp.ts
CHANGED
|
@@ -2,6 +2,58 @@ export type THostedAppHostType = 'onebox' | 'cloudly' | string;
|
|
|
2
2
|
|
|
3
3
|
export const hostedAppOidcRoleClaim = 'roles' as const;
|
|
4
4
|
export const hostedAppOidcAppInstanceIdClaim = 'servezone_app_instance_id' as const;
|
|
5
|
+
export const hostedAppMachineServiceIdClaim = 'servezone_service_id' as const;
|
|
6
|
+
export const hostedAppMachineAudience = 'serve.zone/hosted-app-lifecycle' as const;
|
|
7
|
+
|
|
8
|
+
export interface IHostedAppMachineClaims {
|
|
9
|
+
sub: string;
|
|
10
|
+
aud: typeof hostedAppMachineAudience;
|
|
11
|
+
iat: number;
|
|
12
|
+
exp: number;
|
|
13
|
+
[hostedAppOidcAppInstanceIdClaim]: string;
|
|
14
|
+
[hostedAppMachineServiceIdClaim]: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const createHostedAppMachineSubject = (appInstanceIdArg: string): string => (
|
|
18
|
+
`urn:serve.zone:hosted-app:${appInstanceIdArg}`
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const validateHostedAppMachineClaims = (
|
|
22
|
+
claimsArg: unknown,
|
|
23
|
+
expectedArg: {
|
|
24
|
+
appInstanceId: string;
|
|
25
|
+
serviceId: string;
|
|
26
|
+
nowEpochSeconds?: number;
|
|
27
|
+
},
|
|
28
|
+
): string[] => {
|
|
29
|
+
if (!claimsArg || typeof claimsArg !== 'object' || Array.isArray(claimsArg)) {
|
|
30
|
+
return ['hosted app machine claims must be an object'];
|
|
31
|
+
}
|
|
32
|
+
const claims = claimsArg as Record<string, unknown>;
|
|
33
|
+
const errors: string[] = [];
|
|
34
|
+
if (claims.sub !== createHostedAppMachineSubject(expectedArg.appInstanceId)) {
|
|
35
|
+
errors.push('hosted app machine subject does not match');
|
|
36
|
+
}
|
|
37
|
+
if (claims.aud !== hostedAppMachineAudience) {
|
|
38
|
+
errors.push('hosted app machine audience does not match');
|
|
39
|
+
}
|
|
40
|
+
if (claims[hostedAppOidcAppInstanceIdClaim] !== expectedArg.appInstanceId) {
|
|
41
|
+
errors.push('hosted app machine app instance does not match');
|
|
42
|
+
}
|
|
43
|
+
if (claims[hostedAppMachineServiceIdClaim] !== expectedArg.serviceId) {
|
|
44
|
+
errors.push('hosted app machine service does not match');
|
|
45
|
+
}
|
|
46
|
+
const now = expectedArg.nowEpochSeconds ?? Math.floor(Date.now() / 1000);
|
|
47
|
+
if (!Number.isSafeInteger(claims.iat) || (claims.iat as number) < 0
|
|
48
|
+
|| (claims.iat as number) > now) {
|
|
49
|
+
errors.push('hosted app machine issued-at time is malformed');
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isSafeInteger(claims.exp) || (claims.exp as number) <= now
|
|
52
|
+
|| (Number.isSafeInteger(claims.iat) && (claims.exp as number) <= (claims.iat as number))) {
|
|
53
|
+
errors.push('hosted app machine expiry is malformed or expired');
|
|
54
|
+
}
|
|
55
|
+
return errors;
|
|
56
|
+
};
|
|
5
57
|
|
|
6
58
|
export interface IHostedAppRoleDefinition {
|
|
7
59
|
/** Stable role identifier emitted in the hosted app's audience-scoped token. */
|
|
@@ -276,7 +328,7 @@ export type THostedAppRuntimeStatus =
|
|
|
276
328
|
| 'stopped'
|
|
277
329
|
| 'unknown';
|
|
278
330
|
|
|
279
|
-
export type THostedAppBootstrapActionType = '
|
|
331
|
+
export type THostedAppBootstrapActionType = 'setupRoute' | 'message';
|
|
280
332
|
|
|
281
333
|
export type THostedAppBootstrapActionStatus =
|
|
282
334
|
| 'requested'
|
|
@@ -293,21 +345,12 @@ export type THostedAppUpgradeStatus =
|
|
|
293
345
|
| 'success'
|
|
294
346
|
| 'failed';
|
|
295
347
|
|
|
296
|
-
|
|
297
|
-
appInstanceId: string;
|
|
298
|
-
appControlToken: string;
|
|
299
|
-
hostType?: THostedAppHostType;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export interface IHostedAppBootstrapAction {
|
|
348
|
+
interface IHostedAppBootstrapActionBase {
|
|
303
349
|
id: string;
|
|
304
|
-
|
|
350
|
+
/** Server-managed CAS revision incremented on every action transition. */
|
|
351
|
+
revision: number;
|
|
305
352
|
status: THostedAppBootstrapActionStatus;
|
|
306
353
|
label: string;
|
|
307
|
-
url?: string;
|
|
308
|
-
username?: string;
|
|
309
|
-
password?: string;
|
|
310
|
-
message?: string;
|
|
311
354
|
expiresAt?: number;
|
|
312
355
|
createdAt: number;
|
|
313
356
|
updatedAt: number;
|
|
@@ -315,6 +358,18 @@ export interface IHostedAppBootstrapAction {
|
|
|
315
358
|
dismissedAt?: number;
|
|
316
359
|
}
|
|
317
360
|
|
|
361
|
+
export type IHostedAppBootstrapAction =
|
|
362
|
+
| (IHostedAppBootstrapActionBase & {
|
|
363
|
+
type: 'setupRoute';
|
|
364
|
+
route: string;
|
|
365
|
+
message?: never;
|
|
366
|
+
})
|
|
367
|
+
| (IHostedAppBootstrapActionBase & {
|
|
368
|
+
type: 'message';
|
|
369
|
+
route?: never;
|
|
370
|
+
message: string;
|
|
371
|
+
});
|
|
372
|
+
|
|
318
373
|
export interface IHostedAppUpgradeState {
|
|
319
374
|
status: THostedAppUpgradeStatus;
|
|
320
375
|
appTemplateId?: string;
|
|
@@ -347,9 +402,80 @@ export interface IHostedAppLifecycleState extends IHostedAppLifecycleReport {
|
|
|
347
402
|
upgradeState?: IHostedAppUpgradeState;
|
|
348
403
|
}
|
|
349
404
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
405
|
+
type THostedAppBootstrapActionServerFields =
|
|
406
|
+
| 'id'
|
|
407
|
+
| 'revision'
|
|
408
|
+
| 'status'
|
|
409
|
+
| 'createdAt'
|
|
410
|
+
| 'updatedAt'
|
|
411
|
+
| 'completedAt'
|
|
412
|
+
| 'dismissedAt';
|
|
413
|
+
|
|
414
|
+
export type IHostedAppBootstrapActionRequest =
|
|
415
|
+
| (Omit<
|
|
416
|
+
Extract<IHostedAppBootstrapAction, { type: 'setupRoute' }>,
|
|
417
|
+
THostedAppBootstrapActionServerFields
|
|
418
|
+
> & { id?: string })
|
|
419
|
+
| (Omit<
|
|
420
|
+
Extract<IHostedAppBootstrapAction, { type: 'message' }>,
|
|
421
|
+
THostedAppBootstrapActionServerFields
|
|
422
|
+
> & { id?: string });
|
|
423
|
+
|
|
424
|
+
export const normalizeHostedAppSetupRoute = (routeArg: unknown): string | undefined => {
|
|
425
|
+
if (typeof routeArg !== 'string'
|
|
426
|
+
|| !routeArg.startsWith('/')
|
|
427
|
+
|| routeArg.startsWith('//')
|
|
428
|
+
|| /[\\?#\s]/.test(routeArg)
|
|
429
|
+
|| /%5c/i.test(routeArg)) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
const parsed = new URL(routeArg, 'https://app.invalid');
|
|
433
|
+
return parsed.pathname === routeArg ? routeArg : undefined;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export const validateHostedAppBootstrapActionRequest = (
|
|
437
|
+
actionArg: unknown,
|
|
438
|
+
): string[] => {
|
|
439
|
+
if (!actionArg || typeof actionArg !== 'object' || Array.isArray(actionArg)) {
|
|
440
|
+
return ['hosted app bootstrap action must be an object'];
|
|
441
|
+
}
|
|
442
|
+
const action = actionArg as Record<string, unknown>;
|
|
443
|
+
const errors: string[] = [];
|
|
444
|
+
const allowedKeys = action.type === 'setupRoute'
|
|
445
|
+
? new Set(['id', 'type', 'label', 'route', 'expiresAt'])
|
|
446
|
+
: action.type === 'message'
|
|
447
|
+
? new Set(['id', 'type', 'label', 'message', 'expiresAt'])
|
|
448
|
+
: new Set(['id', 'type', 'label', 'expiresAt']);
|
|
449
|
+
if (Object.keys(action).some((keyArg) => !allowedKeys.has(keyArg))) {
|
|
450
|
+
errors.push('hosted app bootstrap action contains fields outside its schema');
|
|
451
|
+
}
|
|
452
|
+
if (action.id !== undefined
|
|
453
|
+
&& (typeof action.id !== 'string'
|
|
454
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(action.id))) {
|
|
455
|
+
errors.push('hosted app bootstrap action id must be canonical');
|
|
456
|
+
}
|
|
457
|
+
if (action.expiresAt !== undefined
|
|
458
|
+
&& (!Number.isSafeInteger(action.expiresAt) || (action.expiresAt as number) < 1)) {
|
|
459
|
+
errors.push('hosted app bootstrap action expiresAt must be a positive integer');
|
|
460
|
+
}
|
|
461
|
+
if (action.type !== 'setupRoute' && action.type !== 'message') {
|
|
462
|
+
errors.push('hosted app bootstrap action type is unsupported');
|
|
463
|
+
}
|
|
464
|
+
if (typeof action.label !== 'string' || !action.label.trim() || action.label.length > 200) {
|
|
465
|
+
errors.push('hosted app bootstrap action label must be bounded');
|
|
466
|
+
}
|
|
467
|
+
if (action.type === 'setupRoute') {
|
|
468
|
+
if (normalizeHostedAppSetupRoute(action.route) !== action.route) {
|
|
469
|
+
errors.push('hosted app setup route must be a canonical same-origin absolute path');
|
|
470
|
+
}
|
|
471
|
+
if ('message' in action) errors.push('hosted app setup route forbids message');
|
|
472
|
+
}
|
|
473
|
+
if (action.type === 'message') {
|
|
474
|
+
if (typeof action.message !== 'string' || !action.message.trim()
|
|
475
|
+
|| action.message.length > 4000) {
|
|
476
|
+
errors.push('hosted app bootstrap message must be bounded');
|
|
477
|
+
}
|
|
478
|
+
if ('route' in action) errors.push('hosted app message forbids route');
|
|
479
|
+
}
|
|
480
|
+
return errors;
|
|
355
481
|
};
|