@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/changelog.md +17 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/coremail.d.ts +1 -1
- package/dist_ts/data/coremail.runtime.d.ts +4 -1
- package/dist_ts/data/coremail.runtime.js +86 -18
- package/dist_ts/data/immutableimage.d.ts +2 -0
- package/dist_ts/data/immutableimage.js +1 -1
- package/dist_ts/data/secret.d.ts +2 -0
- package/dist_ts/data/secret.js +18 -3
- package/dist_ts/runtime.d.ts +110 -1
- package/dist_ts/runtime.js +507 -2
- package/package.json +1 -1
- package/readme.md +47 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/coremail.runtime.ts +136 -21
- package/ts/data/coremail.ts +1 -1
- package/ts/data/immutableimage.ts +2 -0
- package/ts/data/secret.ts +24 -2
- package/ts/runtime.ts +735 -0
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
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -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 =
|
|
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) {
|
package/ts/data/coremail.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|