@serve.zone/interfaces 23.2.0 → 24.1.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/ts/runtime.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type {
3
3
  IActiveSecretRecipientMetadata,
4
+ IClusterRuntimeTargetSetReadyV1,
5
+ IClusterRuntimeTargetV1,
4
6
  IClusterSecretDeploymentState,
5
7
  ICoreflowRuntimeCapabilities,
6
8
  IIdentityCredential,
@@ -21,7 +23,22 @@ import {
21
23
  verifySecretEnvelopeContext,
22
24
  verifyResolvedSecretManifestDigest,
23
25
  } from './data/secret.js';
26
+ import {
27
+ isClusterRuntimeTargetSetFresh,
28
+ validateClusterRuntimeTargetSet,
29
+ } from './data/clusternode.js';
24
30
  import { isSha256Digest } from './data/immutableimage.js';
31
+ import type {
32
+ IWorkloadInitActiveApprovalAuthorityV1,
33
+ IWorkloadInitApprovalAuthorityReferenceV1,
34
+ IWorkloadInitApprovedArtifactV1,
35
+ } from './runtime.workloadinit.js';
36
+ import {
37
+ validateWorkloadInitActiveApprovalAuthority,
38
+ validateWorkloadInitApprovalAuthorityReference,
39
+ } from './runtime.workloadinit.js';
40
+
41
+ export * from './runtime.workloadinit.js';
25
42
 
26
43
  export interface ISealedResolvedSecretMaterialEntry {
27
44
  secretVersionId: string;
@@ -402,6 +419,130 @@ const computeRuntimeSha256 = async (inputArg: string): Promise<TSha256Digest> =>
402
419
  .join('')}` as TSha256Digest;
403
420
  };
404
421
 
422
+ const computeRuntimeSha256Bytes = async (
423
+ inputArg: Uint8Array,
424
+ ): Promise<TSha256Digest> => {
425
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
426
+ 'SHA-256',
427
+ new Uint8Array(inputArg),
428
+ ));
429
+ return `sha256:${[...digest]
430
+ .map((byteArg) => byteArg.toString(16).padStart(2, '0'))
431
+ .join('')}` as TSha256Digest;
432
+ };
433
+
434
+ /**
435
+ * Reproducible binding persisted inside a trusted Cloudly mutation receipt.
436
+ * This value alone does not prove that Cloudly admitted the envelope.
437
+ */
438
+ export interface ISecretEnvelopeAdmissionBindingV1 {
439
+ schemaVersion: 1;
440
+ recipientKeyId: string;
441
+ recipientGeneration: number;
442
+ envelopeDigest: TSha256Digest;
443
+ requestContextDigest: TSha256Digest;
444
+ }
445
+
446
+ export const createSecretEnvelopeDigestInput = (
447
+ envelopeArg: unknown,
448
+ ): string => {
449
+ const envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
450
+ return JSON.stringify({
451
+ schemaVersion: envelope.schemaVersion,
452
+ profile: envelope.profile,
453
+ recipientKeyId: envelope.recipientKeyId,
454
+ ephemeralPublicKey: envelope.ephemeralPublicKey,
455
+ nonce: envelope.nonce,
456
+ ciphertext: envelope.ciphertext,
457
+ tag: envelope.tag,
458
+ contextDigest: envelope.contextDigest,
459
+ });
460
+ };
461
+
462
+ export const computeSecretEnvelopeDigest = async (
463
+ envelopeArg: unknown,
464
+ ): Promise<TSha256Digest> => computeRuntimeSha256(createSecretEnvelopeDigestInput(envelopeArg));
465
+
466
+ export const computeSecretRequestContextDigest = async (
467
+ contextArg: Uint8Array,
468
+ ): Promise<TSha256Digest> => {
469
+ if (!(contextArg instanceof Uint8Array)) {
470
+ throw new Error('secret request context must be bytes');
471
+ }
472
+ return computeRuntimeSha256Bytes(contextArg);
473
+ };
474
+
475
+ export const validateSecretEnvelopeAdmissionBinding = (
476
+ bindingArg: unknown,
477
+ ): string[] => {
478
+ if (!isRuntimeRecord(bindingArg)
479
+ || !hasExactRuntimeKeys(bindingArg, [
480
+ 'schemaVersion',
481
+ 'recipientKeyId',
482
+ 'recipientGeneration',
483
+ 'envelopeDigest',
484
+ 'requestContextDigest',
485
+ ])) {
486
+ return ['secret envelope admission binding must use its exact schema'];
487
+ }
488
+ const errors: string[] = [];
489
+ if (bindingArg.schemaVersion !== 1) {
490
+ errors.push('secret envelope admission binding schemaVersion must be 1');
491
+ }
492
+ if (!isRuntimeIdentifier(bindingArg.recipientKeyId)
493
+ || !isPositiveSafeInteger(bindingArg.recipientGeneration)) {
494
+ errors.push('secret envelope admission binding recipient must be canonical');
495
+ }
496
+ if (typeof bindingArg.envelopeDigest !== 'string'
497
+ || !isSha256Digest(bindingArg.envelopeDigest)
498
+ || typeof bindingArg.requestContextDigest !== 'string'
499
+ || !isSha256Digest(bindingArg.requestContextDigest)) {
500
+ errors.push('secret envelope admission binding digests must be canonical');
501
+ }
502
+ return errors;
503
+ };
504
+
505
+ export const createSecretEnvelopeAdmissionBinding = async (
506
+ recipientArg: IActiveSecretRecipientMetadata,
507
+ envelopeArg: unknown,
508
+ requestContextArg: Uint8Array,
509
+ ): Promise<ISecretEnvelopeAdmissionBindingV1> => {
510
+ if (validateSecretRecipientMetadata(recipientArg).length > 0
511
+ || recipientArg.lifecycleState !== 'active') {
512
+ throw new Error('secret envelope admission requires the active recipient');
513
+ }
514
+ const envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
515
+ if (envelope.recipientKeyId !== recipientArg.recipientKeyId
516
+ || !await verifySecretEnvelopeContext(envelope, requestContextArg)) {
517
+ throw new Error('secret envelope admission binding does not match recipient and context');
518
+ }
519
+ return {
520
+ schemaVersion: 1,
521
+ recipientKeyId: recipientArg.recipientKeyId,
522
+ recipientGeneration: recipientArg.generation,
523
+ envelopeDigest: await computeSecretEnvelopeDigest(envelope),
524
+ requestContextDigest: await computeSecretRequestContextDigest(requestContextArg),
525
+ };
526
+ };
527
+
528
+ export const verifySecretEnvelopeAdmissionBinding = async (
529
+ bindingArg: unknown,
530
+ envelopeArg: unknown,
531
+ requestContextArg: Uint8Array,
532
+ ): Promise<boolean> => {
533
+ try {
534
+ if (validateSecretEnvelopeAdmissionBinding(bindingArg).length > 0) return false;
535
+ const binding = bindingArg as ISecretEnvelopeAdmissionBindingV1;
536
+ const envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
537
+ return envelope.recipientKeyId === binding.recipientKeyId
538
+ && await verifySecretEnvelopeContext(envelope, requestContextArg)
539
+ && binding.envelopeDigest === await computeSecretEnvelopeDigest(envelope)
540
+ && binding.requestContextDigest === await computeSecretRequestContextDigest(requestContextArg);
541
+ } catch {
542
+ return false;
543
+ }
544
+ };
545
+
405
546
  export type TSecretRecipientEnrollmentStateV1 =
406
547
  | {
407
548
  schemaVersion: 1;
@@ -481,37 +622,42 @@ export const validateSecretRecipientEnrollmentState = (
481
622
  }
482
623
  };
483
624
 
484
- export interface ICoreflowSecretRuntimeNodeTargetV1 {
485
- nodeId: string;
486
- nodeName: string;
487
- platform: TImmutableContainerPlatform;
488
- }
489
-
490
- export interface ICoreflowSecretRuntimeNodeEvidenceV1
491
- extends ICoreflowSecretRuntimeNodeTargetV1 {
492
- workloadInitPlatformManifestDigest: TSha256Digest;
493
- workloadInitExecutableDigest: TSha256Digest;
494
- }
625
+ export interface ICoreflowSecretRuntimeNodeTargetV2
626
+ extends IClusterRuntimeTargetV1 {}
495
627
 
496
- export interface ICoreflowSecretRuntimeArtifactV1 {
497
- platform: TImmutableContainerPlatform;
628
+ export interface ICoreflowSecretRuntimeNodeEvidenceV2
629
+ extends ICoreflowSecretRuntimeNodeTargetV2 {
498
630
  workloadInitPlatformManifestDigest: TSha256Digest;
499
631
  workloadInitExecutableDigest: TSha256Digest;
500
632
  }
501
633
 
502
- export interface ICoreflowSecretRuntimeRegistrationExpectationV1 {
503
- expectationVersion: 1;
634
+ export interface ICoreflowSecretRuntimeRegistrationExpectationV2 {
635
+ expectationVersion: 2;
636
+ expectationDigest: TSha256Digest;
504
637
  reporterSessionId: string;
505
- targets: ICoreflowSecretRuntimeNodeTargetV1[];
506
- workloadInitImageIndexDigest: TSha256Digest;
507
- workloadInitArtifacts: ICoreflowSecretRuntimeArtifactV1[];
638
+ targetAuthority: IClusterRuntimeTargetSetReadyV1;
639
+ workloadInitAuthority: IWorkloadInitActiveApprovalAuthorityV1;
508
640
  activeRecipient: IActiveSecretRecipientMetadata;
509
641
  }
510
642
 
511
- /** Dedicated TypedSocket tag carrying ICoreflowSecretRuntimeRegistrationV1. */
643
+ /** Dedicated TypedSocket tag carrying ICoreflowSecretRuntimeRegistrationV2. */
512
644
  export const coreflowSecretRuntimeRegistrationTagId =
513
645
  'coreflowSecretRuntimeRegistration' as const;
514
646
 
647
+ export type TCoreflowSecretRuntimeRegistrationExpectationResponseV2 =
648
+ | {
649
+ status: 'available';
650
+ expectation: ICoreflowSecretRuntimeRegistrationExpectationV2;
651
+ }
652
+ | {
653
+ status: 'unavailable';
654
+ reason:
655
+ | 'recipient-unavailable'
656
+ | 'workloadinit-unconfigured'
657
+ | 'workloadinit-revoked'
658
+ | 'targets-unavailable';
659
+ };
660
+
515
661
  /** Cluster scope is derived exclusively from the verified cluster JWT. */
516
662
  export interface IReq_GetCoreflowSecretRuntimeRegistrationExpectation
517
663
  extends plugins.typedrequestInterfaces.implementsTR<
@@ -522,9 +668,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
522
668
  request: {
523
669
  identity: IIdentityCredential;
524
670
  };
525
- response: {
526
- expectation: ICoreflowSecretRuntimeRegistrationExpectationV1;
527
- };
671
+ response: TCoreflowSecretRuntimeRegistrationExpectationResponseV2;
528
672
  }
529
673
 
530
674
  export const validateGetCoreflowSecretRuntimeRegistrationExpectationRequest = (
@@ -545,25 +689,29 @@ export const validateGetCoreflowSecretRuntimeRegistrationExpectationRequest = (
545
689
  }
546
690
  };
547
691
 
548
- export interface ICoreflowSecretRuntimeRegistrationV1 {
549
- registrationVersion: 1;
692
+ export interface ICoreflowSecretRuntimeRegistrationV2 {
693
+ registrationVersion: 2;
694
+ expectationDigest: TSha256Digest;
550
695
  reporterSessionId: string;
551
696
  registeredAt: number;
552
- targets: ICoreflowSecretRuntimeNodeTargetV1[];
553
- nodeSetDigest: TSha256Digest;
554
- nodeEvidence: ICoreflowSecretRuntimeNodeEvidenceV1[];
697
+ targetGeneration: number;
698
+ targetSetDigest: TSha256Digest;
699
+ targets: ICoreflowSecretRuntimeNodeTargetV2[];
700
+ nodeEvidence: ICoreflowSecretRuntimeNodeEvidenceV2[];
555
701
  capabilities: ICoreflowRuntimeCapabilities;
556
702
  activeRecipient: IActiveSecretRecipientMetadata;
703
+ workloadInitAuthority: IWorkloadInitApprovalAuthorityReferenceV1;
557
704
  workloadInitImageIndexDigest: TSha256Digest;
558
- workloadInitApprovedAt: number;
559
705
  }
560
706
 
561
707
  const compareRuntimeTargets = (
562
- leftArg: ICoreflowSecretRuntimeNodeTargetV1,
563
- rightArg: ICoreflowSecretRuntimeNodeTargetV1,
708
+ leftArg: ICoreflowSecretRuntimeNodeTargetV2,
709
+ rightArg: ICoreflowSecretRuntimeNodeTargetV2,
564
710
  ): number => {
565
711
  for (const [left, right] of [
566
- [leftArg.nodeId, rightArg.nodeId],
712
+ [leftArg.cloudlyNodeId, rightArg.cloudlyNodeId],
713
+ [leftArg.swarmClusterId, rightArg.swarmClusterId],
714
+ [leftArg.swarmNodeId, rightArg.swarmNodeId],
567
715
  [leftArg.nodeName, rightArg.nodeName],
568
716
  [leftArg.platform, rightArg.platform],
569
717
  ]) {
@@ -574,24 +722,50 @@ const compareRuntimeTargets = (
574
722
  };
575
723
 
576
724
  const canonicalRuntimeTarget = (
577
- targetArg: ICoreflowSecretRuntimeNodeTargetV1,
578
- ): ICoreflowSecretRuntimeNodeTargetV1 => ({
579
- nodeId: targetArg.nodeId,
725
+ targetArg: ICoreflowSecretRuntimeNodeTargetV2,
726
+ ): ICoreflowSecretRuntimeNodeTargetV2 => ({
727
+ cloudlyNodeId: targetArg.cloudlyNodeId,
728
+ swarmClusterId: targetArg.swarmClusterId,
729
+ swarmNodeId: targetArg.swarmNodeId,
580
730
  nodeName: targetArg.nodeName,
581
731
  platform: targetArg.platform,
582
732
  });
583
733
 
584
- export const createCoreflowSecretRuntimeNodeSetDigestInput = (
585
- targetsArg: ICoreflowSecretRuntimeNodeTargetV1[],
734
+ const canonicalActiveRecipient = (
735
+ recipientArg: IActiveSecretRecipientMetadata,
736
+ ): IActiveSecretRecipientMetadata => ({
737
+ schemaVersion: recipientArg.schemaVersion,
738
+ recipientKeyId: recipientArg.recipientKeyId,
739
+ publicKey: recipientArg.publicKey,
740
+ generation: recipientArg.generation,
741
+ activatedAt: recipientArg.activatedAt,
742
+ lifecycleState: recipientArg.lifecycleState,
743
+ });
744
+
745
+ export const createCoreflowSecretRuntimeRegistrationExpectationDigestInput = (
746
+ expectationArg: ICoreflowSecretRuntimeRegistrationExpectationV2,
586
747
  ): string => JSON.stringify({
587
- schemaVersion: 1,
588
- targets: targetsArg.map(canonicalRuntimeTarget),
748
+ schemaVersion: 2,
749
+ purpose: 'serve.zone/coreflow-secret-runtime-expectation',
750
+ reporterSessionId: expectationArg.reporterSessionId,
751
+ targetAuthority: {
752
+ cloudlyClusterId: expectationArg.targetAuthority.cloudlyClusterId,
753
+ swarmClusterId: expectationArg.targetAuthority.swarmClusterId,
754
+ generation: expectationArg.targetAuthority.generation,
755
+ targetSetDigest: expectationArg.targetAuthority.targetSetDigest,
756
+ },
757
+ workloadInitAuthority: {
758
+ authorityVersion: expectationArg.workloadInitAuthority.authorityVersion,
759
+ authorityGeneration: expectationArg.workloadInitAuthority.authorityGeneration,
760
+ approvalDigest: expectationArg.workloadInitAuthority.approvalDigest,
761
+ },
762
+ activeRecipient: canonicalActiveRecipient(expectationArg.activeRecipient),
589
763
  });
590
764
 
591
- export const computeCoreflowSecretRuntimeNodeSetDigest = async (
592
- targetsArg: ICoreflowSecretRuntimeNodeTargetV1[],
765
+ export const computeCoreflowSecretRuntimeRegistrationExpectationDigest = async (
766
+ expectationArg: ICoreflowSecretRuntimeRegistrationExpectationV2,
593
767
  ): Promise<TSha256Digest> => computeRuntimeSha256(
594
- createCoreflowSecretRuntimeNodeSetDigestInput(targetsArg),
768
+ createCoreflowSecretRuntimeRegistrationExpectationDigestInput(expectationArg),
595
769
  );
596
770
 
597
771
  const validateRuntimeTargets = (
@@ -604,178 +778,219 @@ const validateRuntimeTargets = (
604
778
  return [`${pathArg} must be a non-empty bounded array`];
605
779
  }
606
780
  const errors: string[] = [];
607
- let previous: ICoreflowSecretRuntimeNodeTargetV1 | undefined;
608
- const nodeIds = new Set<string>();
781
+ let previous: ICoreflowSecretRuntimeNodeTargetV2 | undefined;
782
+ const cloudlyNodeIds = new Set<string>();
783
+ const swarmNodeScopes = new Set<string>();
609
784
  const nodeNames = new Set<string>();
610
785
  for (const [index, targetArg] of targetsArg.entries()) {
611
786
  if (!isRuntimeRecord(targetArg)
612
- || !hasExactRuntimeKeys(targetArg, ['nodeId', 'nodeName', 'platform'])) {
787
+ || !hasExactRuntimeKeys(targetArg, [
788
+ 'cloudlyNodeId',
789
+ 'swarmClusterId',
790
+ 'swarmNodeId',
791
+ 'nodeName',
792
+ 'platform',
793
+ ])) {
613
794
  errors.push(`${pathArg}[${index}] must use its exact schema`);
614
795
  continue;
615
796
  }
616
- if (!isRuntimeIdentifier(targetArg.nodeId) || !isRuntimeIdentifier(targetArg.nodeName)) {
797
+ if (!isRuntimeIdentifier(targetArg.cloudlyNodeId)
798
+ || !isRuntimeIdentifier(targetArg.swarmClusterId)
799
+ || !isRuntimeIdentifier(targetArg.swarmNodeId)
800
+ || !isRuntimeIdentifier(targetArg.nodeName)) {
617
801
  errors.push(`${pathArg}[${index}] node identity must be canonical`);
618
802
  }
619
803
  if (!runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
620
804
  errors.push(`${pathArg}[${index}].platform must be supported`);
621
805
  }
622
- if (isRuntimeIdentifier(targetArg.nodeId)
806
+ if (isRuntimeIdentifier(targetArg.cloudlyNodeId)
807
+ && isRuntimeIdentifier(targetArg.swarmClusterId)
808
+ && isRuntimeIdentifier(targetArg.swarmNodeId)
623
809
  && isRuntimeIdentifier(targetArg.nodeName)
624
810
  && runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
625
- const target = targetArg as unknown as ICoreflowSecretRuntimeNodeTargetV1;
811
+ const target = targetArg as unknown as ICoreflowSecretRuntimeNodeTargetV2;
626
812
  if (previous && compareRuntimeTargets(previous, target) >= 0) {
627
- errors.push(`${pathArg} must be uniquely sorted by nodeId, nodeName, and platform`);
813
+ errors.push(`${pathArg} must be uniquely sorted by runtime identity`);
628
814
  }
629
815
  previous = target;
630
- if (nodeIds.has(target.nodeId)) errors.push(`${pathArg} nodeIds must be unique`);
816
+ const swarmNodeScope = `${target.swarmClusterId}\0${target.swarmNodeId}`;
817
+ if (cloudlyNodeIds.has(target.cloudlyNodeId)) {
818
+ errors.push(`${pathArg} cloudlyNodeIds must be unique`);
819
+ }
820
+ if (swarmNodeScopes.has(swarmNodeScope)) {
821
+ errors.push(`${pathArg} scoped swarmNodeIds must be unique`);
822
+ }
631
823
  if (nodeNames.has(target.nodeName)) errors.push(`${pathArg} nodeNames must be unique`);
632
- nodeIds.add(target.nodeId);
824
+ cloudlyNodeIds.add(target.cloudlyNodeId);
825
+ swarmNodeScopes.add(swarmNodeScope);
633
826
  nodeNames.add(target.nodeName);
634
827
  }
635
828
  }
636
829
  return errors;
637
830
  };
638
831
 
639
- const validateRuntimeArtifacts = (
640
- artifactsArg: unknown,
641
- pathArg: string,
642
- ): string[] => {
643
- if (!Array.isArray(artifactsArg)
644
- || artifactsArg.length === 0
645
- || artifactsArg.length > runtimePlatforms.size) {
646
- return [`${pathArg} must be a non-empty bounded array`];
647
- }
648
- const errors: string[] = [];
649
- const platforms: string[] = [];
650
- for (const [index, artifactArg] of artifactsArg.entries()) {
651
- if (!isRuntimeRecord(artifactArg)
652
- || !hasExactRuntimeKeys(artifactArg, [
653
- 'platform',
654
- 'workloadInitPlatformManifestDigest',
655
- 'workloadInitExecutableDigest',
656
- ])) {
657
- errors.push(`${pathArg}[${index}] must use its exact schema`);
658
- continue;
659
- }
660
- platforms.push(artifactArg.platform as string);
661
- if (!runtimePlatforms.has(artifactArg.platform as TImmutableContainerPlatform)) {
662
- errors.push(`${pathArg}[${index}].platform must be supported`);
663
- }
664
- if (typeof artifactArg.workloadInitPlatformManifestDigest !== 'string'
665
- || !isSha256Digest(artifactArg.workloadInitPlatformManifestDigest)
666
- || typeof artifactArg.workloadInitExecutableDigest !== 'string'
667
- || !isSha256Digest(artifactArg.workloadInitExecutableDigest)) {
668
- errors.push(`${pathArg}[${index}] digests must be canonical`);
669
- }
670
- }
671
- if (new Set(platforms).size !== platforms.length
672
- || JSON.stringify(platforms) !== JSON.stringify([...platforms].sort())) {
673
- errors.push(`${pathArg} must be uniquely sorted by platform`);
674
- }
675
- return errors;
676
- };
677
-
678
832
  const activeRecipientFingerprint = (recipientArg: IActiveSecretRecipientMetadata): string => (
679
- JSON.stringify({
680
- schemaVersion: recipientArg.schemaVersion,
681
- recipientKeyId: recipientArg.recipientKeyId,
682
- publicKey: recipientArg.publicKey,
683
- generation: recipientArg.generation,
684
- activatedAt: recipientArg.activatedAt,
685
- lifecycleState: recipientArg.lifecycleState,
686
- })
833
+ JSON.stringify(canonicalActiveRecipient(recipientArg))
687
834
  );
688
835
 
689
836
  /**
690
- * Consumers discard this registration on transport disconnect, tag
691
- * removal/replacement, reporter-session change, or any expectation change.
837
+ * Validates only a Cloudly-created expectation. It does not establish the
838
+ * private DSSE trust decision or Spark manager consensus that produced it.
692
839
  */
693
- export const validateCoreflowSecretRuntimeRegistration = async (
694
- registrationArg: unknown,
840
+ export const validateCoreflowSecretRuntimeRegistrationExpectation = async (
695
841
  expectationArg: unknown,
842
+ trustedNowArg: number,
696
843
  ): Promise<string[]> => {
697
844
  try {
698
845
  const errors: string[] = [];
699
846
  if (!isRuntimeRecord(expectationArg)
700
847
  || !hasExactRuntimeKeys(expectationArg, [
701
848
  'expectationVersion',
849
+ 'expectationDigest',
702
850
  'reporterSessionId',
703
- 'targets',
704
- 'workloadInitImageIndexDigest',
705
- 'workloadInitArtifacts',
851
+ 'targetAuthority',
852
+ 'workloadInitAuthority',
706
853
  'activeRecipient',
707
854
  ])) {
708
855
  return ['secret runtime registration expectation must use its exact schema'];
709
856
  }
710
- if (expectationArg.expectationVersion !== 1) {
711
- errors.push('secret runtime registration expectationVersion must be 1');
857
+ if (expectationArg.expectationVersion !== 2) {
858
+ errors.push('secret runtime registration expectationVersion must be 2');
712
859
  }
713
860
  if (!isRuntimeIdentifier(expectationArg.reporterSessionId)) {
714
861
  errors.push('expected secret runtime reporterSessionId must be canonical');
715
862
  }
716
- const expectedTargetErrors = validateRuntimeTargets(expectationArg.targets, 'expected targets');
717
- errors.push(...expectedTargetErrors);
718
- errors.push(...validateRuntimeArtifacts(
719
- expectationArg.workloadInitArtifacts,
720
- 'expected WorkloadInit artifacts',
721
- ));
722
- if (typeof expectationArg.workloadInitImageIndexDigest !== 'string'
723
- || !isSha256Digest(expectationArg.workloadInitImageIndexDigest)) {
724
- errors.push('expected WorkloadInit image index digest must be canonical');
863
+ const targetAuthorityErrors = await validateClusterRuntimeTargetSet(
864
+ expectationArg.targetAuthority,
865
+ );
866
+ errors.push(...targetAuthorityErrors.map((errorArg) => `expected ${errorArg}`));
867
+ if (targetAuthorityErrors.length === 0
868
+ && !isClusterRuntimeTargetSetFresh(
869
+ expectationArg.targetAuthority as IClusterRuntimeTargetSetReadyV1,
870
+ trustedNowArg,
871
+ )) {
872
+ errors.push('expected cluster runtime target authority must be fresh');
725
873
  }
874
+ if (isRuntimeRecord(expectationArg.targetAuthority)
875
+ && expectationArg.targetAuthority.state === 'ready') {
876
+ errors.push(...validateRuntimeTargets(
877
+ expectationArg.targetAuthority.targets,
878
+ 'expected targets',
879
+ ));
880
+ }
881
+ errors.push(...await validateWorkloadInitActiveApprovalAuthority(
882
+ expectationArg.workloadInitAuthority,
883
+ ));
726
884
  const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
727
885
  if (expectedRecipientErrors.length > 0
728
886
  || !isRuntimeRecord(expectationArg.activeRecipient)
729
887
  || expectationArg.activeRecipient.lifecycleState !== 'active') {
730
888
  errors.push('expected secret recipient must be canonical and active');
731
889
  }
890
+ if (isRuntimeRecord(expectationArg.workloadInitAuthority)
891
+ && isRuntimeRecord(expectationArg.workloadInitAuthority.approval)
892
+ && Array.isArray(expectationArg.workloadInitAuthority.approval.artifacts)
893
+ && isRuntimeRecord(expectationArg.targetAuthority)
894
+ && Array.isArray(expectationArg.targetAuthority.targets)) {
895
+ const approvedPlatforms = new Set(
896
+ expectationArg.workloadInitAuthority.approval.artifacts.map((artifactArg) => (
897
+ isRuntimeRecord(artifactArg) ? artifactArg.platform : undefined
898
+ )),
899
+ );
900
+ if (expectationArg.targetAuthority.targets.some((targetArg) => (
901
+ !isRuntimeRecord(targetArg) || !approvedPlatforms.has(targetArg.platform)
902
+ ))) {
903
+ errors.push('expected target platforms must all have approved WorkloadInit artifacts');
904
+ }
905
+ }
906
+ if (typeof expectationArg.expectationDigest !== 'string'
907
+ || !isSha256Digest(expectationArg.expectationDigest)) {
908
+ errors.push('secret runtime expectationDigest must be canonical');
909
+ } else if (errors.length === 0
910
+ && expectationArg.expectationDigest
911
+ !== await computeCoreflowSecretRuntimeRegistrationExpectationDigest(
912
+ expectationArg as unknown as ICoreflowSecretRuntimeRegistrationExpectationV2,
913
+ )) {
914
+ errors.push('secret runtime expectationDigest does not match');
915
+ }
916
+ return errors;
917
+ } catch {
918
+ return ['secret runtime registration expectation must be safely inspectable'];
919
+ }
920
+ };
921
+
922
+ /**
923
+ * Consumers discard this registration on transport disconnect, tag
924
+ * removal/replacement, reporter-session change, or any expectation change.
925
+ */
926
+ export const validateCoreflowSecretRuntimeRegistration = async (
927
+ registrationArg: unknown,
928
+ expectationArg: unknown,
929
+ trustedNowArg: number,
930
+ ): Promise<string[]> => {
931
+ try {
932
+ const errors = await validateCoreflowSecretRuntimeRegistrationExpectation(
933
+ expectationArg,
934
+ trustedNowArg,
935
+ );
936
+ if (errors.length > 0 || !isRuntimeRecord(expectationArg)) return errors;
732
937
  if (!isRuntimeRecord(registrationArg)
733
938
  || !hasExactRuntimeKeys(registrationArg, [
734
939
  'registrationVersion',
940
+ 'expectationDigest',
735
941
  'reporterSessionId',
736
942
  'registeredAt',
943
+ 'targetGeneration',
944
+ 'targetSetDigest',
737
945
  'targets',
738
- 'nodeSetDigest',
739
946
  'nodeEvidence',
740
947
  'capabilities',
741
948
  'activeRecipient',
949
+ 'workloadInitAuthority',
742
950
  'workloadInitImageIndexDigest',
743
- 'workloadInitApprovedAt',
744
951
  ])) {
745
952
  errors.push('secret runtime registration must use its exact schema');
746
953
  return errors;
747
954
  }
748
- if (registrationArg.registrationVersion !== 1) {
749
- errors.push('secret runtime registrationVersion must be 1');
955
+ if (registrationArg.registrationVersion !== 2) {
956
+ errors.push('secret runtime registrationVersion must be 2');
957
+ }
958
+ if (registrationArg.expectationDigest !== expectationArg.expectationDigest) {
959
+ errors.push('secret runtime registration expectationDigest does not match');
750
960
  }
751
961
  if (!isRuntimeIdentifier(registrationArg.reporterSessionId)) {
752
962
  errors.push('secret runtime reporterSessionId must be canonical');
753
963
  } else if (registrationArg.reporterSessionId !== expectationArg.reporterSessionId) {
754
964
  errors.push('secret runtime reporterSessionId does not match the live session');
755
965
  }
756
- if (!isPositiveSafeInteger(registrationArg.registeredAt)
757
- || !isPositiveSafeInteger(registrationArg.workloadInitApprovedAt)) {
758
- errors.push('secret runtime registration timestamps must be positive integers');
966
+ if (!isPositiveSafeInteger(registrationArg.registeredAt)) {
967
+ errors.push('secret runtime registration timestamp must be positive');
759
968
  }
760
969
  const registrationTargetErrors = validateRuntimeTargets(
761
970
  registrationArg.targets,
762
971
  'registration targets',
763
972
  );
764
973
  errors.push(...registrationTargetErrors);
765
- if (typeof registrationArg.nodeSetDigest !== 'string'
766
- || !isSha256Digest(registrationArg.nodeSetDigest)) {
767
- errors.push('secret runtime nodeSetDigest must be canonical');
974
+ const targetAuthority = expectationArg.targetAuthority as unknown as IClusterRuntimeTargetSetReadyV1;
975
+ if (registrationArg.targetGeneration !== targetAuthority.generation
976
+ || registrationArg.targetSetDigest !== targetAuthority.targetSetDigest) {
977
+ errors.push('secret runtime registration target authority does not match');
768
978
  }
769
979
  if (typeof registrationArg.workloadInitImageIndexDigest !== 'string'
770
980
  || !isSha256Digest(registrationArg.workloadInitImageIndexDigest)) {
771
981
  errors.push('secret runtime WorkloadInit image index digest must be canonical');
772
982
  }
773
983
  const registrationRecipientErrors = validateSecretRecipientMetadata(registrationArg.activeRecipient);
984
+ const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
774
985
  if (registrationRecipientErrors.length > 0
775
986
  || !isRuntimeRecord(registrationArg.activeRecipient)
776
987
  || registrationArg.activeRecipient.lifecycleState !== 'active') {
777
988
  errors.push('secret runtime registration recipient must be canonical and active');
778
989
  }
990
+ const registrationAuthorityErrors = validateWorkloadInitApprovalAuthorityReference(
991
+ registrationArg.workloadInitAuthority,
992
+ );
993
+ errors.push(...registrationAuthorityErrors);
779
994
  if (!isRuntimeRecord(registrationArg.capabilities)
780
995
  || !hasOnlyRuntimeKeys(registrationArg.capabilities, [
781
996
  'immutableImageDeploymentVersion',
@@ -801,12 +1016,14 @@ export const validateCoreflowSecretRuntimeRegistration = async (
801
1016
  || registrationArg.nodeEvidence.length > maximumRuntimeNodes) {
802
1017
  errors.push('secret runtime node evidence must be a non-empty bounded array');
803
1018
  } else {
804
- const evidenceTargets: ICoreflowSecretRuntimeNodeTargetV1[] = [];
805
- const evidenceArtifacts: ICoreflowSecretRuntimeArtifactV1[] = [];
1019
+ const evidenceTargets: ICoreflowSecretRuntimeNodeTargetV2[] = [];
1020
+ const evidenceArtifacts: IWorkloadInitApprovedArtifactV1[] = [];
806
1021
  for (const [index, evidenceArg] of registrationArg.nodeEvidence.entries()) {
807
1022
  if (!isRuntimeRecord(evidenceArg)
808
1023
  || !hasExactRuntimeKeys(evidenceArg, [
809
- 'nodeId',
1024
+ 'cloudlyNodeId',
1025
+ 'swarmClusterId',
1026
+ 'swarmNodeId',
810
1027
  'nodeName',
811
1028
  'platform',
812
1029
  'workloadInitPlatformManifestDigest',
@@ -816,7 +1033,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
816
1033
  continue;
817
1034
  }
818
1035
  evidenceTargets.push({
819
- nodeId: evidenceArg.nodeId as string,
1036
+ cloudlyNodeId: evidenceArg.cloudlyNodeId as string,
1037
+ swarmClusterId: evidenceArg.swarmClusterId as string,
1038
+ swarmNodeId: evidenceArg.swarmNodeId as string,
820
1039
  nodeName: evidenceArg.nodeName as string,
821
1040
  platform: evidenceArg.platform as TImmutableContainerPlatform,
822
1041
  });
@@ -839,8 +1058,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
839
1058
  }
840
1059
  const expectedArtifacts = new Map<
841
1060
  TImmutableContainerPlatform,
842
- ICoreflowSecretRuntimeArtifactV1
843
- >((expectationArg.workloadInitArtifacts as ICoreflowSecretRuntimeArtifactV1[])
1061
+ IWorkloadInitApprovedArtifactV1
1062
+ >(((expectationArg.workloadInitAuthority as unknown as IWorkloadInitActiveApprovalAuthorityV1)
1063
+ .approval.artifacts)
844
1064
  .map((artifactArg) => [artifactArg.platform, artifactArg]));
845
1065
  for (const [index, artifactArg] of evidenceArtifacts.entries()) {
846
1066
  const expectedArtifact = expectedArtifacts.get(artifactArg.platform);
@@ -853,21 +1073,26 @@ export const validateCoreflowSecretRuntimeRegistration = async (
853
1073
  }
854
1074
  }
855
1075
  }
856
- if (JSON.stringify(registrationArg.targets) !== JSON.stringify(expectationArg.targets)) {
1076
+ if (JSON.stringify(registrationArg.targets) !== JSON.stringify(targetAuthority.targets)) {
857
1077
  errors.push('secret runtime registration must exactly cover authoritative target nodes');
858
1078
  }
859
- if (expectedTargetErrors.length === 0 && registrationTargetErrors.length === 0) {
860
- const expectedNodeSetDigest = await computeCoreflowSecretRuntimeNodeSetDigest(
861
- expectationArg.targets as ICoreflowSecretRuntimeNodeTargetV1[],
862
- );
863
- if (registrationArg.nodeSetDigest !== expectedNodeSetDigest) {
864
- errors.push('secret runtime nodeSetDigest does not match authoritative target nodes');
865
- }
866
- }
1079
+ const workloadInitAuthority = expectationArg.workloadInitAuthority as
1080
+ IWorkloadInitActiveApprovalAuthorityV1;
867
1081
  if (registrationArg.workloadInitImageIndexDigest
868
- !== expectationArg.workloadInitImageIndexDigest) {
1082
+ !== workloadInitAuthority.approval.workloadInitImageIndexDigest) {
869
1083
  errors.push('secret runtime WorkloadInit image index is not approved');
870
1084
  }
1085
+ if (isRuntimeRecord(registrationArg.workloadInitAuthority)
1086
+ && (
1087
+ registrationArg.workloadInitAuthority.authorityVersion
1088
+ !== workloadInitAuthority.authorityVersion
1089
+ || registrationArg.workloadInitAuthority.authorityGeneration
1090
+ !== workloadInitAuthority.authorityGeneration
1091
+ || registrationArg.workloadInitAuthority.approvalDigest
1092
+ !== workloadInitAuthority.approvalDigest
1093
+ )) {
1094
+ errors.push('secret runtime WorkloadInit authority does not match');
1095
+ }
871
1096
  if (registrationRecipientErrors.length === 0 && expectedRecipientErrors.length === 0
872
1097
  && activeRecipientFingerprint(
873
1098
  registrationArg.activeRecipient as unknown as IActiveSecretRecipientMetadata,