@serve.zone/interfaces 32.14.0 → 32.15.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 +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusternode.d.ts +35 -3
- package/dist_ts/data/clusternode.js +141 -3
- package/dist_ts/data/fleetcutover.d.ts +198 -0
- package/dist_ts/data/fleetcutover.js +570 -1
- package/dist_ts/data/runtimestorageclaim.d.ts +43 -0
- package/dist_ts/data/runtimestorageclaim.js +88 -4
- package/dist_ts/requests/fleetcutover.d.ts +70 -6
- package/dist_ts/requests/runtimesession.d.ts +47 -0
- package/package.json +1 -1
- package/readme.md +80 -16
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +209 -2
- package/ts/data/fleetcutover.ts +842 -0
- package/ts/data/runtimestorageclaim.ts +122 -3
- package/ts/requests/fleetcutover.ts +80 -5
- package/ts/requests/runtimesession.ts +57 -0
package/ts/data/clusternode.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type
|
|
2
|
+
import { isSha256Digest, type TSha256Digest, type TImmutableContainerPlatform } from './immutableimage.js';
|
|
3
3
|
import type { ISparkNodeReportRecord } from './sparknodereport.js';
|
|
4
4
|
import {
|
|
5
|
+
fleetCutoverContract,
|
|
6
|
+
snapshotFleetCutoverPreparedVolumeHandoffAggregate,
|
|
7
|
+
snapshotFleetCutoverPreparedVolumeHandoffPage,
|
|
8
|
+
snapshotFleetCutoverPreparedVolumeHandoffIntent,
|
|
9
|
+
validateFleetCutoverHeldVolumeFenceEvidencePage,
|
|
10
|
+
validateFleetCutoverPreparedVolumeHandoffAggregate,
|
|
11
|
+
validateFleetCutoverPreparedVolumeHandoffPage,
|
|
12
|
+
validateFleetCutoverPreparedVolumeHandoffIntent,
|
|
5
13
|
validateFleetCutoverLocalNodeSnapshot,
|
|
6
14
|
validateFleetCutoverManagerSnapshot,
|
|
15
|
+
type IFleetCutoverAcceptedEvidenceReference,
|
|
16
|
+
type IFleetCutoverHeldVolumeFenceEvidencePage,
|
|
7
17
|
type IFleetCutoverLocalNodeSnapshot,
|
|
8
18
|
type IFleetCutoverManagerSnapshot,
|
|
19
|
+
type IFleetCutoverPreparedVolumeHandoffAggregate,
|
|
20
|
+
type IFleetCutoverPreparedVolumeHandoffIntent,
|
|
21
|
+
type IFleetCutoverPreparedVolumeHandoffPage,
|
|
9
22
|
} from './fleetcutover.js';
|
|
10
23
|
import { protocolRefusalHttpStatus, validateProtocolOfferOn, type IProtocolOffer } from '../protocol.js';
|
|
11
24
|
import {
|
|
@@ -131,9 +144,15 @@ export const sparkSwarmNodeContracts = Object.freeze({
|
|
|
131
144
|
*/
|
|
132
145
|
swarmObservation: Object.freeze({
|
|
133
146
|
endpoint: '/spark/swarm-nodes/swarm-observation',
|
|
134
|
-
maxRequestBytes:
|
|
147
|
+
maxRequestBytes: 4_325_376,
|
|
135
148
|
maxResponseBytes: 2048,
|
|
136
149
|
} as const),
|
|
150
|
+
/** Pinned immutable aggregate page plus at most 32 full handoffs for this authenticated node. */
|
|
151
|
+
fleetCutoverPreparedVolumeHandoffs: Object.freeze({
|
|
152
|
+
endpoint: '/spark/swarm-nodes/fleet-cutover-prepared-volume-handoffs',
|
|
153
|
+
maxRequestBytes: 4096,
|
|
154
|
+
maxResponseBytes: 262_144,
|
|
155
|
+
} as const),
|
|
137
156
|
protocolRefusalStatus: protocolRefusalHttpStatus,
|
|
138
157
|
} as const);
|
|
139
158
|
|
|
@@ -221,6 +240,8 @@ interface ISparkSwarmObservationBase {
|
|
|
221
240
|
reporterSessionId: string;
|
|
222
241
|
observationSequence: number;
|
|
223
242
|
observedAt: number;
|
|
243
|
+
/** Available in every observation state; exact page membership proves an empty node subset. */
|
|
244
|
+
fleetCutoverHeldVolumeFences?: IFleetCutoverHeldVolumeFenceEvidencePage;
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
export type TSparkSwarmObservation =
|
|
@@ -288,6 +309,32 @@ export interface ISparkSwarmObservationRequest {
|
|
|
288
309
|
observation: TSparkSwarmObservation;
|
|
289
310
|
}
|
|
290
311
|
|
|
312
|
+
/** First read may select current with null; all following page reads pin the exact aggregate. */
|
|
313
|
+
export interface ISparkGetFleetCutoverPreparedVolumeHandoffsRequest {
|
|
314
|
+
nodeId: string;
|
|
315
|
+
nodeToken: string;
|
|
316
|
+
protocol: IProtocolOffer;
|
|
317
|
+
aggregate: IFleetCutoverAcceptedEvidenceReference | null;
|
|
318
|
+
pageIndex: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export type TSparkGetFleetCutoverPreparedVolumeHandoffsResponse =
|
|
322
|
+
| {
|
|
323
|
+
aggregate: null;
|
|
324
|
+
page: null;
|
|
325
|
+
nodeId: string;
|
|
326
|
+
nodeHandoffs: [];
|
|
327
|
+
protocol: IProtocolOffer;
|
|
328
|
+
}
|
|
329
|
+
| {
|
|
330
|
+
aggregate: IFleetCutoverPreparedVolumeHandoffAggregate;
|
|
331
|
+
page: IFleetCutoverPreparedVolumeHandoffPage;
|
|
332
|
+
nodeId: string;
|
|
333
|
+
/** Exact complete authenticated-node subset committed by this page; may be empty. */
|
|
334
|
+
nodeHandoffs: IFleetCutoverPreparedVolumeHandoffIntent[];
|
|
335
|
+
protocol: IProtocolOffer;
|
|
336
|
+
};
|
|
337
|
+
|
|
291
338
|
export interface ISparkSwarmObservationAcceptedReceipt {
|
|
292
339
|
state: 'accepted';
|
|
293
340
|
nodeId: string;
|
|
@@ -579,6 +626,8 @@ export const validateSparkSwarmObservation = async (
|
|
|
579
626
|
'observationSequence',
|
|
580
627
|
'observedAt',
|
|
581
628
|
'state',
|
|
629
|
+
...(observation.fleetCutoverHeldVolumeFences === undefined
|
|
630
|
+
? [] : ['fleetCutoverHeldVolumeFences']),
|
|
582
631
|
];
|
|
583
632
|
const expectedKeys = observation.state === 'unknown'
|
|
584
633
|
? [...commonKeys, 'reason']
|
|
@@ -619,6 +668,14 @@ export const validateSparkSwarmObservation = async (
|
|
|
619
668
|
.includes(observation.reason as string)) {
|
|
620
669
|
errors.push('Spark Swarm observation unknown reason must be canonical');
|
|
621
670
|
}
|
|
671
|
+
if (observation.fleetCutoverHeldVolumeFences !== undefined) {
|
|
672
|
+
const evidenceErrors = await validateFleetCutoverHeldVolumeFenceEvidencePage(
|
|
673
|
+
observation.fleetCutoverHeldVolumeFences,
|
|
674
|
+
);
|
|
675
|
+
errors.push(...evidenceErrors.map(
|
|
676
|
+
errorArg => `Spark Swarm observation ${errorArg}`,
|
|
677
|
+
));
|
|
678
|
+
}
|
|
622
679
|
if (observation.state === 'member') {
|
|
623
680
|
if (!isRuntimeIdentifier(observation.localSwarmNodeId)
|
|
624
681
|
|| typeof observation.controlAvailable !== 'boolean') {
|
|
@@ -694,6 +751,151 @@ const validateSparkReporterIdentity = (
|
|
|
694
751
|
return errors;
|
|
695
752
|
};
|
|
696
753
|
|
|
754
|
+
const readFleetCutoverEvidenceReference = (
|
|
755
|
+
valueArg: unknown,
|
|
756
|
+
): IFleetCutoverAcceptedEvidenceReference | null => {
|
|
757
|
+
if (valueArg === null) return null;
|
|
758
|
+
if (!isRuntimeRecord(valueArg)
|
|
759
|
+
|| !hasExactKeys(valueArg, ['id', 'generation', 'digest'])
|
|
760
|
+
|| !isRuntimeIdentifier(valueArg.id)
|
|
761
|
+
|| !isPositiveSafeInteger(valueArg.generation)
|
|
762
|
+
|| typeof valueArg.digest !== 'string'
|
|
763
|
+
|| !isSha256Digest(valueArg.digest)) return null;
|
|
764
|
+
return {
|
|
765
|
+
id: valueArg.id,
|
|
766
|
+
generation: valueArg.generation,
|
|
767
|
+
digest: valueArg.digest,
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
export const validateSparkGetFleetCutoverPreparedVolumeHandoffsRequest = (
|
|
772
|
+
requestArg: unknown,
|
|
773
|
+
): string[] => {
|
|
774
|
+
try {
|
|
775
|
+
if (!isRuntimeRecord(requestArg)
|
|
776
|
+
|| !hasExactKeys(requestArg, [
|
|
777
|
+
'nodeId', 'nodeToken', 'protocol', 'aggregate', 'pageIndex',
|
|
778
|
+
])) {
|
|
779
|
+
return ['Spark prepared volume handoff request must use its exact schema'];
|
|
780
|
+
}
|
|
781
|
+
canonicalizeStrictJson(
|
|
782
|
+
requestArg,
|
|
783
|
+
failSparkSwarmObservationCanonicalization,
|
|
784
|
+
'Spark prepared volume handoff request',
|
|
785
|
+
);
|
|
786
|
+
const errors = validateProtocolOfferOn(
|
|
787
|
+
requestArg,
|
|
788
|
+
'Spark prepared volume handoff request protocol',
|
|
789
|
+
);
|
|
790
|
+
errors.push(...validateSparkReporterIdentity(
|
|
791
|
+
requestArg,
|
|
792
|
+
'Spark prepared volume handoff request',
|
|
793
|
+
));
|
|
794
|
+
if ((requestArg.aggregate !== null
|
|
795
|
+
&& readFleetCutoverEvidenceReference(requestArg.aggregate) === null)
|
|
796
|
+
|| typeof requestArg.pageIndex !== 'number'
|
|
797
|
+
|| !Number.isSafeInteger(requestArg.pageIndex)
|
|
798
|
+
|| requestArg.pageIndex < 0
|
|
799
|
+
|| requestArg.pageIndex >= fleetCutoverContract.maximumPreparedVolumeHandoffPages) {
|
|
800
|
+
errors.push('Spark prepared volume handoff request pin must be canonical');
|
|
801
|
+
}
|
|
802
|
+
return errors;
|
|
803
|
+
} catch {
|
|
804
|
+
return ['Spark prepared volume handoff request must be safely inspectable'];
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
export const validateSparkGetFleetCutoverPreparedVolumeHandoffsResponse = async (
|
|
809
|
+
responseArg: unknown,
|
|
810
|
+
requestArg: ISparkGetFleetCutoverPreparedVolumeHandoffsRequest,
|
|
811
|
+
): Promise<string[]> => {
|
|
812
|
+
try {
|
|
813
|
+
if (validateSparkGetFleetCutoverPreparedVolumeHandoffsRequest(requestArg).length) {
|
|
814
|
+
return ['expected Spark prepared volume handoff request must be valid'];
|
|
815
|
+
}
|
|
816
|
+
if (!isRuntimeRecord(responseArg)
|
|
817
|
+
|| !hasExactKeys(responseArg, [
|
|
818
|
+
'aggregate', 'page', 'nodeId', 'nodeHandoffs', 'protocol',
|
|
819
|
+
])) {
|
|
820
|
+
return ['Spark prepared volume handoff response must use its exact schema'];
|
|
821
|
+
}
|
|
822
|
+
const canonical = canonicalizeStrictJson(
|
|
823
|
+
responseArg,
|
|
824
|
+
failSparkSwarmObservationCanonicalization,
|
|
825
|
+
'Spark prepared volume handoff response',
|
|
826
|
+
);
|
|
827
|
+
const errors = validateProtocolOfferOn(
|
|
828
|
+
responseArg,
|
|
829
|
+
'Spark prepared volume handoff response protocol',
|
|
830
|
+
);
|
|
831
|
+
if (new TextEncoder().encode(canonical).byteLength
|
|
832
|
+
> sparkSwarmNodeContracts.fleetCutoverPreparedVolumeHandoffs.maxResponseBytes) {
|
|
833
|
+
errors.push('Spark prepared volume handoff response exceeds its encoded byte bound');
|
|
834
|
+
}
|
|
835
|
+
if (responseArg.nodeId !== requestArg.nodeId) {
|
|
836
|
+
errors.push('Spark prepared volume handoff response must name the authenticated node');
|
|
837
|
+
}
|
|
838
|
+
if (responseArg.aggregate === null) {
|
|
839
|
+
if (requestArg.aggregate !== null || responseArg.page !== null
|
|
840
|
+
|| !Array.isArray(responseArg.nodeHandoffs)
|
|
841
|
+
|| responseArg.nodeHandoffs.length !== 0) {
|
|
842
|
+
errors.push('Spark empty prepared volume handoff response must be canonical');
|
|
843
|
+
}
|
|
844
|
+
return errors;
|
|
845
|
+
}
|
|
846
|
+
const aggregate = snapshotFleetCutoverPreparedVolumeHandoffAggregate(responseArg.aggregate);
|
|
847
|
+
const page = snapshotFleetCutoverPreparedVolumeHandoffPage(responseArg.page);
|
|
848
|
+
if ((await validateFleetCutoverPreparedVolumeHandoffAggregate(aggregate)).length
|
|
849
|
+
|| aggregate.state !== 'complete') {
|
|
850
|
+
errors.push('Spark prepared volume handoff response aggregate must be complete and valid');
|
|
851
|
+
}
|
|
852
|
+
if ((await validateFleetCutoverPreparedVolumeHandoffPage(page)).length) {
|
|
853
|
+
errors.push('Spark prepared volume handoff response page must be valid');
|
|
854
|
+
}
|
|
855
|
+
if (requestArg.aggregate !== null
|
|
856
|
+
&& (requestArg.aggregate.id !== aggregate.id
|
|
857
|
+
|| requestArg.aggregate.generation !== aggregate.generation
|
|
858
|
+
|| requestArg.aggregate.digest !== aggregate.digest)) {
|
|
859
|
+
errors.push('Spark prepared volume handoff response must preserve the aggregate pin');
|
|
860
|
+
}
|
|
861
|
+
if (page.aggregate.id !== aggregate.id
|
|
862
|
+
|| page.aggregate.generation !== aggregate.generation
|
|
863
|
+
|| page.aggregate.digest !== aggregate.digest
|
|
864
|
+
|| page.pageIndex !== requestArg.pageIndex
|
|
865
|
+
|| page.pageIndex >= aggregate.pageCount) {
|
|
866
|
+
errors.push('Spark prepared volume handoff response page must bind the requested aggregate page');
|
|
867
|
+
}
|
|
868
|
+
if (!Array.isArray(responseArg.nodeHandoffs)
|
|
869
|
+
|| responseArg.nodeHandoffs.length
|
|
870
|
+
> fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries) {
|
|
871
|
+
errors.push('Spark prepared volume handoff response node subset must be bounded');
|
|
872
|
+
return errors;
|
|
873
|
+
}
|
|
874
|
+
const expectedNodeHandoffs = page.handoffs.filter(
|
|
875
|
+
referenceArg => referenceArg.nodeId === requestArg.nodeId,
|
|
876
|
+
);
|
|
877
|
+
if (responseArg.nodeHandoffs.length !== expectedNodeHandoffs.length) {
|
|
878
|
+
errors.push('Spark prepared volume handoff response must include the complete node subset');
|
|
879
|
+
return errors;
|
|
880
|
+
}
|
|
881
|
+
for (const [index, handoffArg] of responseArg.nodeHandoffs.entries()) {
|
|
882
|
+
const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
|
|
883
|
+
const expected = expectedNodeHandoffs[index];
|
|
884
|
+
if ((await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
|
|
885
|
+
|| handoff.nodeId !== requestArg.nodeId
|
|
886
|
+
|| handoff.id !== expected.id
|
|
887
|
+
|| handoff.generation !== expected.generation
|
|
888
|
+
|| handoff.digest !== expected.digest) {
|
|
889
|
+
errors.push('Spark prepared volume handoff response node subset must exactly match page order and authority');
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return errors;
|
|
894
|
+
} catch {
|
|
895
|
+
return ['Spark prepared volume handoff response must be safely inspectable'];
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
697
899
|
export const validateSparkSwarmObservationRequest = async (
|
|
698
900
|
requestArg: unknown,
|
|
699
901
|
): Promise<string[]> => {
|
|
@@ -720,6 +922,11 @@ export const validateSparkSwarmObservationRequest = async (
|
|
|
720
922
|
requestArg,
|
|
721
923
|
'Spark Swarm observation request',
|
|
722
924
|
));
|
|
925
|
+
if (isRuntimeRecord(requestArg.observation)
|
|
926
|
+
&& isRuntimeRecord(requestArg.observation.fleetCutoverHeldVolumeFences)
|
|
927
|
+
&& requestArg.observation.fleetCutoverHeldVolumeFences.nodeId !== requestArg.nodeId) {
|
|
928
|
+
errors.push('Spark Swarm held volume fence evidence must name the authenticated node');
|
|
929
|
+
}
|
|
723
930
|
return errors;
|
|
724
931
|
} catch {
|
|
725
932
|
return ['Spark Swarm observation request must be safely inspectable'];
|