@serve.zone/interfaces 23.0.3 → 23.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/readme.md CHANGED
@@ -67,6 +67,46 @@ Primary v23 additions include:
67
67
  - Mandatory `IImmutableContainerInvocationV1` evidence on immutable image
68
68
  deployment plans.
69
69
 
70
+ ### Secrets v23.1 Runtime Registration And Reporting
71
+
72
+ The Node-only `@serve.zone/interfaces/runtime` export adds the live contract
73
+ that Cloudly must validate before publishing secret-bearing desired state to a
74
+ Coreflow connection:
75
+
76
+ - `getSecretRecipientEnrollmentState` returns either an exact generation-zero
77
+ empty state or the complete valid recipient set for the cluster derived from
78
+ the verified JWT.
79
+ - `ICoreflowSecretRuntimeRegistrationV1` binds a reporter session to the exact
80
+ authoritative schedulable target-node set, required secret capabilities,
81
+ active recipient, approved WorkloadInit OCI index, and exact per-platform
82
+ manifest and installed-executable digests.
83
+ - `validateCoreflowSecretRuntimeRegistration` compares the registration with a
84
+ trusted expectation built by Cloudly, including the live reporter session.
85
+ Missing, extra, duplicate, reordered, or mismatched node evidence fails
86
+ closed. Consumers must discard the registration on transport disconnect, tag
87
+ removal or replacement, or any live session, placement, artifact, or
88
+ recipient expectation change before publishing more secret-bearing state.
89
+ - `reportSecretDeploymentState` reports only `applying`, `applied`, `drifted`,
90
+ or `failed` for the manifest and plan revision selected by Cloudly. The
91
+ validator receives the trusted cluster ID after JWT verification and the
92
+ trusted live reporter session. Wire-provided manifest scope is checked
93
+ against, and never replaces, that trusted authority.
94
+
95
+ The package validates report shape, digest, trusted cluster, and trusted live
96
+ session; it does not persist replay state or mutate deployment plans. Deployment
97
+ report consumers must persist an atomic receipt keyed by the verified cluster,
98
+ reporter session, service, and positive sequence. The report digest uses
99
+ fixed-order JSON and excludes only the JWT identity and `reportDigest`. The
100
+ consumer accepts the exact next sequence once, returns the prior response for a
101
+ same-digest replay, rejects a different-digest replay, and ensures a plan
102
+ revision CAS failure consumes neither the sequence nor a receipt. Timestamps
103
+ are informational and never replace live session, placement, artifact,
104
+ recipient, sequence, or plan-revision fences.
105
+
106
+ `createWorkloadInitEnvironmentMap` now rejects manifests without any
107
+ `launcher-environment` delivery. Consumers must bypass WorkloadInit for
108
+ file-only manifests.
109
+
70
110
  ## Portable Storage Contracts
71
111
 
72
112
  App Store templates can declare logical, template-local `storageClasses` and
@@ -547,6 +587,13 @@ the exact `allowedOperations` derived from
547
587
  `data.coreMailWorkloadOperationPolicy`. Disabled bindings never authenticate;
548
588
  draining bindings permit outbound status plus inbound list/fetch/ack only.
549
589
 
590
+ Gateway recipient resolution uses four strict outcomes. `accept`, `defer`, and
591
+ `reject` are authoritative only for recipients owned by an active binding.
592
+ `unhandled` means the CoreMail peer does not own that recipient, so the gateway
593
+ may continue its next configured resolver. Consumers must apply
594
+ `data.normalizeCoreMailRecipientResolutions()` against the exact requested
595
+ recipient set before acting on a peer response.
596
+
550
597
  Large content never travels inside TypedRequest JSON. Outbound body parts and
551
598
  attachments use prepare/upload/complete operations with short-lived one-time
552
599
  HTTP transfer grants. Inbound delivery uses bounded delivery listing followed
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '23.0.3',
6
+ version: '23.1.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -27,6 +27,7 @@ import {
27
27
  type ICoreMailMailbox,
28
28
  type ICoreMailOutboundMessageDescriptor,
29
29
  type ICoreMailPartStatus,
30
+ type ICoreMailRecipientResolution,
30
31
  type ICoreMailRuntimeKeyReference,
31
32
  type ICoreMailReconciliationStatus,
32
33
  type ICoreMailReplicaIdentity,
@@ -77,6 +78,7 @@ export const coreMailContractLimits = Object.freeze({
77
78
  maximumFilenameBytes: 512,
78
79
  maximumContentIdBytes: 256,
79
80
  maximumBearerTokenBytes: 1_024,
81
+ maximumRecipientResolutionMessageBytes: 512,
80
82
  } as const);
81
83
 
82
84
  export class CoreMailContractError extends Error {
@@ -587,6 +589,139 @@ export const normalizeCoreMailEnvelope = (
587
589
  return deepFreezeValue({ mailFrom, rcptTo });
588
590
  };
589
591
 
592
+ const requireCoreMailOpaqueToken = (valueArg: unknown, pathArg: string): string => {
593
+ const value = requireString(
594
+ valueArg,
595
+ pathArg,
596
+ coreMailContractLimits.maximumBearerTokenBytes,
597
+ );
598
+ let decodedValue: string;
599
+ try {
600
+ decodedValue = globalThis.atob(
601
+ `${value.replace(/-/g, '+').replace(/_/g, '/')}${'='.repeat((4 - (value.length % 4)) % 4)}`,
602
+ );
603
+ } catch {
604
+ return fail(`${pathArg} must use ${coreMailTransferTokenPolicy.format}`);
605
+ }
606
+ const canonicalValue = globalThis.btoa(decodedValue)
607
+ .replace(/\+/g, '-')
608
+ .replace(/\//g, '_')
609
+ .replace(/=+$/u, '');
610
+ if (
611
+ decodedValue.length !== coreMailTransferTokenPolicy.decodedBytes
612
+ || value.length !== coreMailTransferTokenPolicy.encodedCharacters
613
+ || canonicalValue !== value
614
+ ) {
615
+ fail(`${pathArg} must use ${coreMailTransferTokenPolicy.format}`);
616
+ }
617
+ return value;
618
+ };
619
+
620
+ export const normalizeCoreMailRecipientResolution = (
621
+ valueArg: unknown,
622
+ pathArg = 'recipientResolution',
623
+ ): ICoreMailRecipientResolution => {
624
+ const record = readRecord(valueArg, pathArg);
625
+ const actionDescriptor = Object.getOwnPropertyDescriptor(record, 'action');
626
+ if (
627
+ !actionDescriptor
628
+ || !actionDescriptor.enumerable
629
+ || !Object.hasOwn(actionDescriptor, 'value')
630
+ ) {
631
+ return fail(`${pathArg}.action must be an enumerable data property`);
632
+ }
633
+ const action = actionDescriptor.value;
634
+ if (action === 'accept') {
635
+ assertKeys(record, pathArg, ['recipient', 'action', 'routingHandle']);
636
+ return deepFreezeValue({
637
+ recipient: requireCanonicalMailbox(record.recipient, `${pathArg}.recipient`),
638
+ action,
639
+ routingHandle: requireCoreMailOpaqueToken(
640
+ record.routingHandle,
641
+ `${pathArg}.routingHandle`,
642
+ ),
643
+ });
644
+ }
645
+ if (action === 'reject' || action === 'defer') {
646
+ assertKeys(record, pathArg, ['recipient', 'action', 'smtpCode', 'message']);
647
+ const minimumCode = action === 'reject' ? 500 : 400;
648
+ const maximumCode = action === 'reject' ? 599 : 499;
649
+ return deepFreezeValue({
650
+ recipient: requireCanonicalMailbox(record.recipient, `${pathArg}.recipient`),
651
+ action,
652
+ smtpCode: requireSafeInteger(
653
+ record.smtpCode,
654
+ `${pathArg}.smtpCode`,
655
+ minimumCode,
656
+ maximumCode,
657
+ ),
658
+ message: requireDisplayText(
659
+ record.message,
660
+ `${pathArg}.message`,
661
+ coreMailContractLimits.maximumRecipientResolutionMessageBytes,
662
+ ),
663
+ });
664
+ }
665
+ if (action === 'unhandled') {
666
+ assertKeys(record, pathArg, ['recipient', 'action']);
667
+ return deepFreezeValue({
668
+ recipient: requireCanonicalMailbox(record.recipient, `${pathArg}.recipient`),
669
+ action,
670
+ });
671
+ }
672
+ return fail(`${pathArg}.action must be accept, reject, defer, or unhandled`);
673
+ };
674
+
675
+ export const normalizeCoreMailRecipientResolutions = (
676
+ valueArg: unknown,
677
+ expectedRecipientsArg: readonly string[],
678
+ pathArg = 'recipientResolutions',
679
+ ): readonly ICoreMailRecipientResolution[] => {
680
+ const expectedRecipients = readArray(
681
+ expectedRecipientsArg,
682
+ `${pathArg}.expectedRecipients`,
683
+ coreMailLimits.recipientCount,
684
+ ).map((recipientArg, indexArg) =>
685
+ requireCanonicalMailbox(
686
+ recipientArg,
687
+ `${pathArg}.expectedRecipients[${indexArg}]`,
688
+ ),
689
+ );
690
+ if (
691
+ expectedRecipients.length === 0
692
+ || new Set(expectedRecipients).size !== expectedRecipients.length
693
+ ) {
694
+ fail(`${pathArg}.expectedRecipients must contain unique recipients`);
695
+ }
696
+ const resolutions = readArray(
697
+ valueArg,
698
+ pathArg,
699
+ coreMailLimits.recipientCount,
700
+ ).map((resolutionArg, indexArg) =>
701
+ normalizeCoreMailRecipientResolution(resolutionArg, `${pathArg}[${indexArg}]`),
702
+ );
703
+ const byRecipient = new Map<string, ICoreMailRecipientResolution>();
704
+ for (const resolution of resolutions) {
705
+ if (byRecipient.has(resolution.recipient)) {
706
+ fail(`${pathArg} contains a duplicate recipient resolution`);
707
+ }
708
+ byRecipient.set(resolution.recipient, resolution);
709
+ }
710
+ if (
711
+ resolutions.length !== expectedRecipients.length
712
+ || resolutions.some((resolutionArg) => !expectedRecipients.includes(resolutionArg.recipient))
713
+ ) {
714
+ fail(`${pathArg} must resolve the exact expected recipient set`);
715
+ }
716
+ return deepFreezeValue(expectedRecipients.map((recipientArg) => {
717
+ const resolution = byRecipient.get(recipientArg);
718
+ if (!resolution) {
719
+ return fail(`${pathArg} is missing a recipient resolution`);
720
+ }
721
+ return { ...resolution };
722
+ }));
723
+ };
724
+
590
725
  const forbiddenHeaderNames = new Set([
591
726
  'bcc',
592
727
  'cc',
@@ -864,30 +999,10 @@ export const normalizeCoreMailTransferGrant = (
864
999
  if (path !== `${coreMailTransferProtocol.pathPrefix}${grantId}`) {
865
1000
  fail(`${pathArg}.path must match its grantId`);
866
1001
  }
867
- const bearerToken = requireString(
1002
+ const bearerToken = requireCoreMailOpaqueToken(
868
1003
  record.bearerToken,
869
1004
  `${pathArg}.bearerToken`,
870
- coreMailContractLimits.maximumBearerTokenBytes,
871
1005
  );
872
- let decodedToken: string;
873
- try {
874
- decodedToken = globalThis.atob(
875
- `${bearerToken.replace(/-/g, '+').replace(/_/g, '/')}${'='.repeat((4 - (bearerToken.length % 4)) % 4)}`,
876
- );
877
- } catch {
878
- return fail(`${pathArg}.bearerToken must use ${coreMailTransferTokenPolicy.format}`);
879
- }
880
- const canonicalToken = globalThis.btoa(decodedToken)
881
- .replace(/\+/g, '-')
882
- .replace(/\//g, '_')
883
- .replace(/=+$/u, '');
884
- if (
885
- decodedToken.length !== coreMailTransferTokenPolicy.decodedBytes
886
- || bearerToken.length !== coreMailTransferTokenPolicy.encodedCharacters
887
- || canonicalToken !== bearerToken
888
- ) {
889
- fail(`${pathArg}.bearerToken must use ${coreMailTransferTokenPolicy.format}`);
890
- }
891
1006
  const issuedAt = requireSafeInteger(record.issuedAt, `${pathArg}.issuedAt`, 1);
892
1007
  const expiresAt = requireSafeInteger(record.expiresAt, `${pathArg}.expiresAt`, 1);
893
1008
  if (expiresAt - issuedAt !== coreMailLimits.transferGrantTtlMs) {
@@ -28,7 +28,7 @@ export type TCoreMailSubmissionState =
28
28
  | 'deadLettered';
29
29
  export type TCoreMailInboundDeliveryState = 'pending' | 'fetching' | 'fetched' | 'acknowledged';
30
30
  export type TCoreMailInboundAckOutcome = 'processed' | 'discarded';
31
- export type TCoreMailRecipientAction = 'accept' | 'reject' | 'defer';
31
+ export type TCoreMailRecipientAction = 'accept' | 'reject' | 'defer' | 'unhandled';
32
32
  export type TCoreMailReconciliationState = 'applying' | 'ready' | 'failed';
33
33
  export type TCoreMailWorkloadOperation =
34
34
  | 'coreMailPrepareOutboundSubmission'
@@ -71,6 +71,8 @@ export interface ICoreflowRuntimeCapabilities {
71
71
  sealedSecretMaterialVersion: 1;
72
72
  secretRecipientEnrollmentVersion: 1;
73
73
  workloadInitEnvironmentVersion: 1;
74
+ /** Coreflow can submit replay-safe schema-v1 secret deployment reports. */
75
+ secretDeploymentReportVersion?: 1;
74
76
  }
75
77
 
76
78
  export type TImmutableContainerPlatform = 'linux/amd64' | 'linux/arm64';
package/ts/data/secret.ts CHANGED
@@ -922,7 +922,7 @@ export const validateResolvedSecretManifest = (
922
922
  return errors;
923
923
  };
924
924
 
925
- const validateResolvedSecretManifestReference = (
925
+ const validateResolvedSecretManifestReferenceAtPath = (
926
926
  referenceArg: unknown,
927
927
  expectedClusterIdArg: string,
928
928
  pathArg: string,
@@ -958,6 +958,25 @@ const validateResolvedSecretManifestReference = (
958
958
  return [];
959
959
  };
960
960
 
961
+ /** Strict public validator for a cluster-scoped resolved manifest reference. */
962
+ export const validateResolvedSecretManifestReference = (
963
+ referenceArg: unknown,
964
+ expectedClusterIdArg: string,
965
+ ): string[] => {
966
+ try {
967
+ if (!isSecretIdentifier(expectedClusterIdArg)) {
968
+ return ['expected cluster id must be canonical'];
969
+ }
970
+ return validateResolvedSecretManifestReferenceAtPath(
971
+ referenceArg,
972
+ expectedClusterIdArg,
973
+ 'resolved secret manifest reference',
974
+ );
975
+ } catch {
976
+ return ['resolved secret manifest reference must be safely inspectable'];
977
+ }
978
+ };
979
+
961
980
  export const validateServiceSecretDeploymentPlan = (
962
981
  planArg: unknown,
963
982
  expectedTargetClusterIdsArg?: string[],
@@ -992,7 +1011,7 @@ export const validateServiceSecretDeploymentPlan = (
992
1011
  expectedClusterIdArg: string,
993
1012
  pathArg: string,
994
1013
  ): void => {
995
- const referenceErrors = validateResolvedSecretManifestReference(
1014
+ const referenceErrors = validateResolvedSecretManifestReferenceAtPath(
996
1015
  referenceArg,
997
1016
  expectedClusterIdArg,
998
1017
  pathArg,
@@ -1183,6 +1202,9 @@ export const createWorkloadInitEnvironmentMap = async (
1183
1202
  source: createLauncherSecretSourcePath(resource),
1184
1203
  };
1185
1204
  }));
1205
+ if (entries.length === 0) {
1206
+ throw new Error('workload init map requires at least one launcher-environment delivery');
1207
+ }
1186
1208
  entries.sort((leftArg, rightArg) => (
1187
1209
  leftArg.variableName < rightArg.variableName
1188
1210
  ? -1