@serve.zone/interfaces 22.0.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.
Files changed (75) hide show
  1. package/changelog.md +12 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/appstore/types.d.ts +31 -16
  4. package/dist_ts/appstore/types.js +208 -11
  5. package/dist_ts/data/deploymentoperation.js +6 -20
  6. package/dist_ts/data/deploymentpreflight.d.ts +2 -6
  7. package/dist_ts/data/deploymentpreflight.js +1 -1
  8. package/dist_ts/data/hostedapp.d.ts +40 -14
  9. package/dist_ts/data/hostedapp.js +90 -1
  10. package/dist_ts/data/immutableimage.d.ts +30 -1
  11. package/dist_ts/data/immutableimage.js +103 -1
  12. package/dist_ts/data/index.d.ts +0 -2
  13. package/dist_ts/data/index.js +1 -3
  14. package/dist_ts/data/secret.d.ts +186 -13
  15. package/dist_ts/data/secret.js +543 -45
  16. package/dist_ts/data/service.d.ts +0 -39
  17. package/dist_ts/data/service.js +1 -77
  18. package/dist_ts/data/settings.d.ts +62 -43
  19. package/dist_ts/data/settings.js +34 -2
  20. package/dist_ts/platform/storage.d.ts +13 -14
  21. package/dist_ts/platform/storage.js +1 -1
  22. package/dist_ts/platform/storagemigration.d.ts +8 -8
  23. package/dist_ts/platform/storagemigration.golden.d.ts +2 -2
  24. package/dist_ts/platform/storagemigration.golden.js +19 -88
  25. package/dist_ts/platform/storagemigration.js +46 -58
  26. package/dist_ts/platform/types.d.ts +5 -19
  27. package/dist_ts/plugins.d.ts +2 -1
  28. package/dist_ts/plugins.js +3 -2
  29. package/dist_ts/requests/hostedapp.d.ts +12 -9
  30. package/dist_ts/requests/hostedapp.js +38 -1
  31. package/dist_ts/requests/index.d.ts +1 -3
  32. package/dist_ts/requests/index.js +2 -4
  33. package/dist_ts/requests/platform.d.ts +0 -1
  34. package/dist_ts/requests/secret.d.ts +23 -17
  35. package/dist_ts/requests/secret.js +161 -2
  36. package/dist_ts/requests/service.d.ts +0 -13
  37. package/dist_ts/requests/settings.d.ts +3 -3
  38. package/dist_ts/runtime.d.ts +78 -26
  39. package/dist_ts/runtime.js +219 -19
  40. package/package.json +2 -1
  41. package/readme.md +106 -28
  42. package/ts/00_commitinfo_data.ts +1 -1
  43. package/ts/appstore/types.ts +251 -25
  44. package/ts/data/deploymentoperation.ts +7 -31
  45. package/ts/data/deploymentpreflight.ts +2 -5
  46. package/ts/data/hostedapp.ts +144 -18
  47. package/ts/data/immutableimage.ts +150 -1
  48. package/ts/data/index.ts +0 -2
  49. package/ts/data/secret.ts +775 -53
  50. package/ts/data/service.ts +0 -118
  51. package/ts/data/settings.ts +70 -80
  52. package/ts/platform/storage.ts +15 -16
  53. package/ts/platform/storagemigration.golden.ts +18 -95
  54. package/ts/platform/storagemigration.ts +57 -87
  55. package/ts/platform/types.ts +6 -19
  56. package/ts/plugins.ts +2 -0
  57. package/ts/requests/hostedapp.ts +50 -9
  58. package/ts/requests/index.ts +0 -4
  59. package/ts/requests/platform.ts +0 -1
  60. package/ts/requests/secret.ts +204 -9
  61. package/ts/requests/service.ts +0 -16
  62. package/ts/requests/settings.ts +3 -3
  63. package/ts/runtime.ts +314 -41
  64. package/dist_ts/data/secretbundle.d.ts +0 -57
  65. package/dist_ts/data/secretbundle.js +0 -2
  66. package/dist_ts/data/secretgroup.d.ts +0 -52
  67. package/dist_ts/data/secretgroup.js +0 -2
  68. package/dist_ts/requests/secretbundle.d.ts +0 -75
  69. package/dist_ts/requests/secretbundle.js +0 -4
  70. package/dist_ts/requests/secretgroup.d.ts +0 -52
  71. package/dist_ts/requests/secretgroup.js +0 -4
  72. package/ts/data/secretbundle.ts +0 -65
  73. package/ts/data/secretgroup.ts +0 -58
  74. package/ts/requests/secretbundle.ts +0 -103
  75. package/ts/requests/secretgroup.ts +0 -74
@@ -1,4 +1,13 @@
1
- import type { IServiceSecretFile } from '../data/service.js';
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
- bindingsV1: 'storage.bindings.v1',
48
+ bindingsV2: 'storage.bindings.v2',
40
49
  filesystemV1: 'storage.filesystem.v1',
41
- objectStorageV1: 'storage.object-storage.v1',
42
- objectStorageSecretFileV1: 'storage.object-storage.secret-file.v1',
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
- export interface IAppStoreEnvVar {
152
+ interface IAppStoreEnvironmentDeclarationBase {
144
153
  key: string;
145
- value?: string;
146
154
  description: string;
147
155
  required?: boolean;
148
- /** Store the submitted value in the service-owned Cloudly secret bundle, never in service environment data. */
149
- secret?: boolean;
150
- /** Generate a value server-side when the installer does not submit one. Generation is allowed only for secrets. */
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
- type: 'randomHex';
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 = validateAppStorePlatformOidcCapability(config.platformOidc);
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 secretFileKeys = Array.isArray(config.secretFiles)
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?: IAppStoreEnvVar[];
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?: string;
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
- envVars?: Record<string, string>;
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;
@@ -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, validateServiceSecretFiles } from './service.js';
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, 8, (platformArg) => (
166
- /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(platformArg)
167
- ))) {
168
- errors.push('targetPlatforms must be unique canonical platform identifiers');
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: string[];
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'>;
@@ -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 = 'setupLink' | 'credentials' | 'message';
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
- export interface IHostedAppRuntimeIdentity {
297
- appInstanceId: string;
298
- appControlToken: string;
299
- hostType?: THostedAppHostType;
300
- }
301
-
302
- export interface IHostedAppBootstrapAction {
348
+ interface IHostedAppBootstrapActionBase {
303
349
  id: string;
304
- type: THostedAppBootstrapActionType;
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
- export type IHostedAppBootstrapActionRequest = Omit<
351
- IHostedAppBootstrapAction,
352
- 'id' | 'status' | 'createdAt' | 'updatedAt' | 'completedAt' | 'dismissedAt'
353
- > & {
354
- id?: string;
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
  };