@serve.zone/interfaces 23.1.0 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,52 +622,96 @@ export const validateSecretRecipientEnrollmentState = (
481
622
  }
482
623
  };
483
624
 
484
- export interface ICoreflowSecretRuntimeNodeTargetV1 {
485
- nodeId: string;
486
- nodeName: string;
487
- platform: TImmutableContainerPlatform;
488
- }
625
+ export interface ICoreflowSecretRuntimeNodeTargetV2
626
+ extends IClusterRuntimeTargetV1 {}
489
627
 
490
- export interface ICoreflowSecretRuntimeNodeEvidenceV1
491
- extends ICoreflowSecretRuntimeNodeTargetV1 {
628
+ export interface ICoreflowSecretRuntimeNodeEvidenceV2
629
+ extends ICoreflowSecretRuntimeNodeTargetV2 {
492
630
  workloadInitPlatformManifestDigest: TSha256Digest;
493
631
  workloadInitExecutableDigest: TSha256Digest;
494
632
  }
495
633
 
496
- export interface ICoreflowSecretRuntimeArtifactV1 {
497
- platform: TImmutableContainerPlatform;
498
- workloadInitPlatformManifestDigest: TSha256Digest;
499
- workloadInitExecutableDigest: TSha256Digest;
500
- }
501
-
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
- export interface ICoreflowSecretRuntimeRegistrationV1 {
512
- registrationVersion: 1;
643
+ /** Dedicated TypedSocket tag carrying ICoreflowSecretRuntimeRegistrationV2. */
644
+ export const coreflowSecretRuntimeRegistrationTagId =
645
+ 'coreflowSecretRuntimeRegistration' as const;
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
+
661
+ /** Cluster scope is derived exclusively from the verified cluster JWT. */
662
+ export interface IReq_GetCoreflowSecretRuntimeRegistrationExpectation
663
+ extends plugins.typedrequestInterfaces.implementsTR<
664
+ plugins.typedrequestInterfaces.ITypedRequest,
665
+ IReq_GetCoreflowSecretRuntimeRegistrationExpectation
666
+ > {
667
+ method: 'getCoreflowSecretRuntimeRegistrationExpectation';
668
+ request: {
669
+ identity: IIdentityCredential;
670
+ };
671
+ response: TCoreflowSecretRuntimeRegistrationExpectationResponseV2;
672
+ }
673
+
674
+ export const validateGetCoreflowSecretRuntimeRegistrationExpectationRequest = (
675
+ requestArg: unknown,
676
+ ): string[] => {
677
+ try {
678
+ if (!isRuntimeRecord(requestArg)
679
+ || !hasExactRuntimeKeys(requestArg, ['identity'])
680
+ || !isRuntimeRecord(requestArg.identity)
681
+ || !hasExactRuntimeKeys(requestArg.identity, ['jwt'])
682
+ || typeof requestArg.identity.jwt !== 'string'
683
+ || requestArg.identity.jwt.length === 0) {
684
+ return ['secret runtime registration expectation request must contain only a JWT identity'];
685
+ }
686
+ return [];
687
+ } catch {
688
+ return ['secret runtime registration expectation request must be safely inspectable'];
689
+ }
690
+ };
691
+
692
+ export interface ICoreflowSecretRuntimeRegistrationV2 {
693
+ registrationVersion: 2;
694
+ expectationDigest: TSha256Digest;
513
695
  reporterSessionId: string;
514
696
  registeredAt: number;
515
- targets: ICoreflowSecretRuntimeNodeTargetV1[];
516
- nodeSetDigest: TSha256Digest;
517
- nodeEvidence: ICoreflowSecretRuntimeNodeEvidenceV1[];
697
+ targetGeneration: number;
698
+ targetSetDigest: TSha256Digest;
699
+ targets: ICoreflowSecretRuntimeNodeTargetV2[];
700
+ nodeEvidence: ICoreflowSecretRuntimeNodeEvidenceV2[];
518
701
  capabilities: ICoreflowRuntimeCapabilities;
519
702
  activeRecipient: IActiveSecretRecipientMetadata;
703
+ workloadInitAuthority: IWorkloadInitApprovalAuthorityReferenceV1;
520
704
  workloadInitImageIndexDigest: TSha256Digest;
521
- workloadInitApprovedAt: number;
522
705
  }
523
706
 
524
707
  const compareRuntimeTargets = (
525
- leftArg: ICoreflowSecretRuntimeNodeTargetV1,
526
- rightArg: ICoreflowSecretRuntimeNodeTargetV1,
708
+ leftArg: ICoreflowSecretRuntimeNodeTargetV2,
709
+ rightArg: ICoreflowSecretRuntimeNodeTargetV2,
527
710
  ): number => {
528
711
  for (const [left, right] of [
529
- [leftArg.nodeId, rightArg.nodeId],
712
+ [leftArg.cloudlyNodeId, rightArg.cloudlyNodeId],
713
+ [leftArg.swarmClusterId, rightArg.swarmClusterId],
714
+ [leftArg.swarmNodeId, rightArg.swarmNodeId],
530
715
  [leftArg.nodeName, rightArg.nodeName],
531
716
  [leftArg.platform, rightArg.platform],
532
717
  ]) {
@@ -537,24 +722,50 @@ const compareRuntimeTargets = (
537
722
  };
538
723
 
539
724
  const canonicalRuntimeTarget = (
540
- targetArg: ICoreflowSecretRuntimeNodeTargetV1,
541
- ): ICoreflowSecretRuntimeNodeTargetV1 => ({
542
- nodeId: targetArg.nodeId,
725
+ targetArg: ICoreflowSecretRuntimeNodeTargetV2,
726
+ ): ICoreflowSecretRuntimeNodeTargetV2 => ({
727
+ cloudlyNodeId: targetArg.cloudlyNodeId,
728
+ swarmClusterId: targetArg.swarmClusterId,
729
+ swarmNodeId: targetArg.swarmNodeId,
543
730
  nodeName: targetArg.nodeName,
544
731
  platform: targetArg.platform,
545
732
  });
546
733
 
547
- export const createCoreflowSecretRuntimeNodeSetDigestInput = (
548
- 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,
549
747
  ): string => JSON.stringify({
550
- schemaVersion: 1,
551
- 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),
552
763
  });
553
764
 
554
- export const computeCoreflowSecretRuntimeNodeSetDigest = async (
555
- targetsArg: ICoreflowSecretRuntimeNodeTargetV1[],
765
+ export const computeCoreflowSecretRuntimeRegistrationExpectationDigest = async (
766
+ expectationArg: ICoreflowSecretRuntimeRegistrationExpectationV2,
556
767
  ): Promise<TSha256Digest> => computeRuntimeSha256(
557
- createCoreflowSecretRuntimeNodeSetDigestInput(targetsArg),
768
+ createCoreflowSecretRuntimeRegistrationExpectationDigestInput(expectationArg),
558
769
  );
559
770
 
560
771
  const validateRuntimeTargets = (
@@ -567,178 +778,219 @@ const validateRuntimeTargets = (
567
778
  return [`${pathArg} must be a non-empty bounded array`];
568
779
  }
569
780
  const errors: string[] = [];
570
- let previous: ICoreflowSecretRuntimeNodeTargetV1 | undefined;
571
- const nodeIds = new Set<string>();
781
+ let previous: ICoreflowSecretRuntimeNodeTargetV2 | undefined;
782
+ const cloudlyNodeIds = new Set<string>();
783
+ const swarmNodeScopes = new Set<string>();
572
784
  const nodeNames = new Set<string>();
573
785
  for (const [index, targetArg] of targetsArg.entries()) {
574
786
  if (!isRuntimeRecord(targetArg)
575
- || !hasExactRuntimeKeys(targetArg, ['nodeId', 'nodeName', 'platform'])) {
787
+ || !hasExactRuntimeKeys(targetArg, [
788
+ 'cloudlyNodeId',
789
+ 'swarmClusterId',
790
+ 'swarmNodeId',
791
+ 'nodeName',
792
+ 'platform',
793
+ ])) {
576
794
  errors.push(`${pathArg}[${index}] must use its exact schema`);
577
795
  continue;
578
796
  }
579
- if (!isRuntimeIdentifier(targetArg.nodeId) || !isRuntimeIdentifier(targetArg.nodeName)) {
797
+ if (!isRuntimeIdentifier(targetArg.cloudlyNodeId)
798
+ || !isRuntimeIdentifier(targetArg.swarmClusterId)
799
+ || !isRuntimeIdentifier(targetArg.swarmNodeId)
800
+ || !isRuntimeIdentifier(targetArg.nodeName)) {
580
801
  errors.push(`${pathArg}[${index}] node identity must be canonical`);
581
802
  }
582
803
  if (!runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
583
804
  errors.push(`${pathArg}[${index}].platform must be supported`);
584
805
  }
585
- if (isRuntimeIdentifier(targetArg.nodeId)
806
+ if (isRuntimeIdentifier(targetArg.cloudlyNodeId)
807
+ && isRuntimeIdentifier(targetArg.swarmClusterId)
808
+ && isRuntimeIdentifier(targetArg.swarmNodeId)
586
809
  && isRuntimeIdentifier(targetArg.nodeName)
587
810
  && runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
588
- const target = targetArg as unknown as ICoreflowSecretRuntimeNodeTargetV1;
811
+ const target = targetArg as unknown as ICoreflowSecretRuntimeNodeTargetV2;
589
812
  if (previous && compareRuntimeTargets(previous, target) >= 0) {
590
- errors.push(`${pathArg} must be uniquely sorted by nodeId, nodeName, and platform`);
813
+ errors.push(`${pathArg} must be uniquely sorted by runtime identity`);
591
814
  }
592
815
  previous = target;
593
- 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
+ }
594
823
  if (nodeNames.has(target.nodeName)) errors.push(`${pathArg} nodeNames must be unique`);
595
- nodeIds.add(target.nodeId);
824
+ cloudlyNodeIds.add(target.cloudlyNodeId);
825
+ swarmNodeScopes.add(swarmNodeScope);
596
826
  nodeNames.add(target.nodeName);
597
827
  }
598
828
  }
599
829
  return errors;
600
830
  };
601
831
 
602
- const validateRuntimeArtifacts = (
603
- artifactsArg: unknown,
604
- pathArg: string,
605
- ): string[] => {
606
- if (!Array.isArray(artifactsArg)
607
- || artifactsArg.length === 0
608
- || artifactsArg.length > runtimePlatforms.size) {
609
- return [`${pathArg} must be a non-empty bounded array`];
610
- }
611
- const errors: string[] = [];
612
- const platforms: string[] = [];
613
- for (const [index, artifactArg] of artifactsArg.entries()) {
614
- if (!isRuntimeRecord(artifactArg)
615
- || !hasExactRuntimeKeys(artifactArg, [
616
- 'platform',
617
- 'workloadInitPlatformManifestDigest',
618
- 'workloadInitExecutableDigest',
619
- ])) {
620
- errors.push(`${pathArg}[${index}] must use its exact schema`);
621
- continue;
622
- }
623
- platforms.push(artifactArg.platform as string);
624
- if (!runtimePlatforms.has(artifactArg.platform as TImmutableContainerPlatform)) {
625
- errors.push(`${pathArg}[${index}].platform must be supported`);
626
- }
627
- if (typeof artifactArg.workloadInitPlatformManifestDigest !== 'string'
628
- || !isSha256Digest(artifactArg.workloadInitPlatformManifestDigest)
629
- || typeof artifactArg.workloadInitExecutableDigest !== 'string'
630
- || !isSha256Digest(artifactArg.workloadInitExecutableDigest)) {
631
- errors.push(`${pathArg}[${index}] digests must be canonical`);
632
- }
633
- }
634
- if (new Set(platforms).size !== platforms.length
635
- || JSON.stringify(platforms) !== JSON.stringify([...platforms].sort())) {
636
- errors.push(`${pathArg} must be uniquely sorted by platform`);
637
- }
638
- return errors;
639
- };
640
-
641
832
  const activeRecipientFingerprint = (recipientArg: IActiveSecretRecipientMetadata): string => (
642
- JSON.stringify({
643
- schemaVersion: recipientArg.schemaVersion,
644
- recipientKeyId: recipientArg.recipientKeyId,
645
- publicKey: recipientArg.publicKey,
646
- generation: recipientArg.generation,
647
- activatedAt: recipientArg.activatedAt,
648
- lifecycleState: recipientArg.lifecycleState,
649
- })
833
+ JSON.stringify(canonicalActiveRecipient(recipientArg))
650
834
  );
651
835
 
652
836
  /**
653
- * Consumers discard this registration on transport disconnect, tag
654
- * 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.
655
839
  */
656
- export const validateCoreflowSecretRuntimeRegistration = async (
657
- registrationArg: unknown,
840
+ export const validateCoreflowSecretRuntimeRegistrationExpectation = async (
658
841
  expectationArg: unknown,
842
+ trustedNowArg: number,
659
843
  ): Promise<string[]> => {
660
844
  try {
661
845
  const errors: string[] = [];
662
846
  if (!isRuntimeRecord(expectationArg)
663
847
  || !hasExactRuntimeKeys(expectationArg, [
664
848
  'expectationVersion',
849
+ 'expectationDigest',
665
850
  'reporterSessionId',
666
- 'targets',
667
- 'workloadInitImageIndexDigest',
668
- 'workloadInitArtifacts',
851
+ 'targetAuthority',
852
+ 'workloadInitAuthority',
669
853
  'activeRecipient',
670
854
  ])) {
671
855
  return ['secret runtime registration expectation must use its exact schema'];
672
856
  }
673
- if (expectationArg.expectationVersion !== 1) {
674
- errors.push('secret runtime registration expectationVersion must be 1');
857
+ if (expectationArg.expectationVersion !== 2) {
858
+ errors.push('secret runtime registration expectationVersion must be 2');
675
859
  }
676
860
  if (!isRuntimeIdentifier(expectationArg.reporterSessionId)) {
677
861
  errors.push('expected secret runtime reporterSessionId must be canonical');
678
862
  }
679
- const expectedTargetErrors = validateRuntimeTargets(expectationArg.targets, 'expected targets');
680
- errors.push(...expectedTargetErrors);
681
- errors.push(...validateRuntimeArtifacts(
682
- expectationArg.workloadInitArtifacts,
683
- 'expected WorkloadInit artifacts',
684
- ));
685
- if (typeof expectationArg.workloadInitImageIndexDigest !== 'string'
686
- || !isSha256Digest(expectationArg.workloadInitImageIndexDigest)) {
687
- 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');
688
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
+ ));
689
884
  const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
690
885
  if (expectedRecipientErrors.length > 0
691
886
  || !isRuntimeRecord(expectationArg.activeRecipient)
692
887
  || expectationArg.activeRecipient.lifecycleState !== 'active') {
693
888
  errors.push('expected secret recipient must be canonical and active');
694
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;
695
937
  if (!isRuntimeRecord(registrationArg)
696
938
  || !hasExactRuntimeKeys(registrationArg, [
697
939
  'registrationVersion',
940
+ 'expectationDigest',
698
941
  'reporterSessionId',
699
942
  'registeredAt',
943
+ 'targetGeneration',
944
+ 'targetSetDigest',
700
945
  'targets',
701
- 'nodeSetDigest',
702
946
  'nodeEvidence',
703
947
  'capabilities',
704
948
  'activeRecipient',
949
+ 'workloadInitAuthority',
705
950
  'workloadInitImageIndexDigest',
706
- 'workloadInitApprovedAt',
707
951
  ])) {
708
952
  errors.push('secret runtime registration must use its exact schema');
709
953
  return errors;
710
954
  }
711
- if (registrationArg.registrationVersion !== 1) {
712
- 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');
713
960
  }
714
961
  if (!isRuntimeIdentifier(registrationArg.reporterSessionId)) {
715
962
  errors.push('secret runtime reporterSessionId must be canonical');
716
963
  } else if (registrationArg.reporterSessionId !== expectationArg.reporterSessionId) {
717
964
  errors.push('secret runtime reporterSessionId does not match the live session');
718
965
  }
719
- if (!isPositiveSafeInteger(registrationArg.registeredAt)
720
- || !isPositiveSafeInteger(registrationArg.workloadInitApprovedAt)) {
721
- 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');
722
968
  }
723
969
  const registrationTargetErrors = validateRuntimeTargets(
724
970
  registrationArg.targets,
725
971
  'registration targets',
726
972
  );
727
973
  errors.push(...registrationTargetErrors);
728
- if (typeof registrationArg.nodeSetDigest !== 'string'
729
- || !isSha256Digest(registrationArg.nodeSetDigest)) {
730
- 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');
731
978
  }
732
979
  if (typeof registrationArg.workloadInitImageIndexDigest !== 'string'
733
980
  || !isSha256Digest(registrationArg.workloadInitImageIndexDigest)) {
734
981
  errors.push('secret runtime WorkloadInit image index digest must be canonical');
735
982
  }
736
983
  const registrationRecipientErrors = validateSecretRecipientMetadata(registrationArg.activeRecipient);
984
+ const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
737
985
  if (registrationRecipientErrors.length > 0
738
986
  || !isRuntimeRecord(registrationArg.activeRecipient)
739
987
  || registrationArg.activeRecipient.lifecycleState !== 'active') {
740
988
  errors.push('secret runtime registration recipient must be canonical and active');
741
989
  }
990
+ const registrationAuthorityErrors = validateWorkloadInitApprovalAuthorityReference(
991
+ registrationArg.workloadInitAuthority,
992
+ );
993
+ errors.push(...registrationAuthorityErrors);
742
994
  if (!isRuntimeRecord(registrationArg.capabilities)
743
995
  || !hasOnlyRuntimeKeys(registrationArg.capabilities, [
744
996
  'immutableImageDeploymentVersion',
@@ -764,12 +1016,14 @@ export const validateCoreflowSecretRuntimeRegistration = async (
764
1016
  || registrationArg.nodeEvidence.length > maximumRuntimeNodes) {
765
1017
  errors.push('secret runtime node evidence must be a non-empty bounded array');
766
1018
  } else {
767
- const evidenceTargets: ICoreflowSecretRuntimeNodeTargetV1[] = [];
768
- const evidenceArtifacts: ICoreflowSecretRuntimeArtifactV1[] = [];
1019
+ const evidenceTargets: ICoreflowSecretRuntimeNodeTargetV2[] = [];
1020
+ const evidenceArtifacts: IWorkloadInitApprovedArtifactV1[] = [];
769
1021
  for (const [index, evidenceArg] of registrationArg.nodeEvidence.entries()) {
770
1022
  if (!isRuntimeRecord(evidenceArg)
771
1023
  || !hasExactRuntimeKeys(evidenceArg, [
772
- 'nodeId',
1024
+ 'cloudlyNodeId',
1025
+ 'swarmClusterId',
1026
+ 'swarmNodeId',
773
1027
  'nodeName',
774
1028
  'platform',
775
1029
  'workloadInitPlatformManifestDigest',
@@ -779,7 +1033,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
779
1033
  continue;
780
1034
  }
781
1035
  evidenceTargets.push({
782
- nodeId: evidenceArg.nodeId as string,
1036
+ cloudlyNodeId: evidenceArg.cloudlyNodeId as string,
1037
+ swarmClusterId: evidenceArg.swarmClusterId as string,
1038
+ swarmNodeId: evidenceArg.swarmNodeId as string,
783
1039
  nodeName: evidenceArg.nodeName as string,
784
1040
  platform: evidenceArg.platform as TImmutableContainerPlatform,
785
1041
  });
@@ -802,8 +1058,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
802
1058
  }
803
1059
  const expectedArtifacts = new Map<
804
1060
  TImmutableContainerPlatform,
805
- ICoreflowSecretRuntimeArtifactV1
806
- >((expectationArg.workloadInitArtifacts as ICoreflowSecretRuntimeArtifactV1[])
1061
+ IWorkloadInitApprovedArtifactV1
1062
+ >(((expectationArg.workloadInitAuthority as unknown as IWorkloadInitActiveApprovalAuthorityV1)
1063
+ .approval.artifacts)
807
1064
  .map((artifactArg) => [artifactArg.platform, artifactArg]));
808
1065
  for (const [index, artifactArg] of evidenceArtifacts.entries()) {
809
1066
  const expectedArtifact = expectedArtifacts.get(artifactArg.platform);
@@ -816,21 +1073,26 @@ export const validateCoreflowSecretRuntimeRegistration = async (
816
1073
  }
817
1074
  }
818
1075
  }
819
- if (JSON.stringify(registrationArg.targets) !== JSON.stringify(expectationArg.targets)) {
1076
+ if (JSON.stringify(registrationArg.targets) !== JSON.stringify(targetAuthority.targets)) {
820
1077
  errors.push('secret runtime registration must exactly cover authoritative target nodes');
821
1078
  }
822
- if (expectedTargetErrors.length === 0 && registrationTargetErrors.length === 0) {
823
- const expectedNodeSetDigest = await computeCoreflowSecretRuntimeNodeSetDigest(
824
- expectationArg.targets as ICoreflowSecretRuntimeNodeTargetV1[],
825
- );
826
- if (registrationArg.nodeSetDigest !== expectedNodeSetDigest) {
827
- errors.push('secret runtime nodeSetDigest does not match authoritative target nodes');
828
- }
829
- }
1079
+ const workloadInitAuthority = expectationArg.workloadInitAuthority as
1080
+ IWorkloadInitActiveApprovalAuthorityV1;
830
1081
  if (registrationArg.workloadInitImageIndexDigest
831
- !== expectationArg.workloadInitImageIndexDigest) {
1082
+ !== workloadInitAuthority.approval.workloadInitImageIndexDigest) {
832
1083
  errors.push('secret runtime WorkloadInit image index is not approved');
833
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
+ }
834
1096
  if (registrationRecipientErrors.length === 0 && expectedRecipientErrors.length === 0
835
1097
  && activeRecipientFingerprint(
836
1098
  registrationArg.activeRecipient as unknown as IActiveSecretRecipientMetadata,