@serve.zone/interfaces 32.13.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.
@@ -6,6 +6,17 @@ import type {
6
6
  IRuntimeAuthorityReference,
7
7
  } from './runtimeassignment.js';
8
8
  import type { IRuntimeSessionBinding } from './runtimesession.js';
9
+ import type { IRuntimeStorageClaim, IRuntimeStorageClaimIntent } from './runtimestorageclaim.js';
10
+ import {
11
+ bindRuntimeStorageClaimToIntent,
12
+ snapshotRuntimeStorageClaim,
13
+ snapshotRuntimeStorageClaimIntent,
14
+ validateRuntimeStorageClaim,
15
+ validateRuntimeStorageClaimIntent,
16
+ } from './runtimestorageclaim.js';
17
+ import type { IAppStoreFilesystemStorageRequest } from '../appstore/types.js';
18
+ import { normalizeStorageRequest } from '../platform/storage.js';
19
+ import type { ISecretVersionReference } from './secret.js';
9
20
  import {
10
21
  canonicalizeStrictJson,
11
22
  createCanonicalJsonSha256Hex,
@@ -280,6 +291,101 @@ export interface IFleetCutoverVolumeHandoffIntent {
280
291
  digest: TSha256Digest;
281
292
  }
282
293
 
294
+ export interface IFleetCutoverPersistentVolumeMountDescriptor
295
+ extends IFleetCutoverMountDescriptor {
296
+ type: 'volume';
297
+ classification: 'persistent-volume';
298
+ readOnly: false;
299
+ }
300
+
301
+ /**
302
+ * Cloudly-owned desired handoff prepared from authenticated legacy observations. Observed Docker
303
+ * source identity is not physical storage lineage; Corestore proves lineage later under the held
304
+ * native attachment fence.
305
+ */
306
+ export interface IFleetCutoverPreparedVolumeHandoffIntent {
307
+ id: string;
308
+ generation: number;
309
+ clusterId: string;
310
+ nodeId: string;
311
+ serviceId: string;
312
+ volumeId: string;
313
+ observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
314
+ claimIntent: IRuntimeStorageClaimIntent;
315
+ runtime: IClusterRuntimeReference & { phase: 'draining' };
316
+ scope: IFleetCutoverAcceptedEvidenceReference;
317
+ targetSet: IFleetCutoverTargetSetReference;
318
+ legacyStorageEvidence: IFleetCutoverAcceptedEvidenceReference;
319
+ sourceNodeObservation: IFleetCutoverAcceptedEvidenceReference;
320
+ acceptedAt: number;
321
+ digest: TSha256Digest;
322
+ }
323
+
324
+ export interface IFleetCutoverPreparedVolumeHandoffUpsert {
325
+ nodeId: string;
326
+ serviceId: string;
327
+ mountIdentityDigest: TSha256Digest;
328
+ request: IAppStoreFilesystemStorageRequest & {
329
+ accessMode: 'ReadWriteOnce';
330
+ reclaimPolicy: 'retain';
331
+ };
332
+ }
333
+
334
+ /** One bounded CAS mutation; the durable aggregate may contain many such 32-entry batches. */
335
+ export interface IFleetCutoverPreparedVolumeHandoffMutation {
336
+ mutationId: string;
337
+ clusterId: string;
338
+ currentRuntime: IClusterRuntimeReference & { phase: 'draining' };
339
+ scope: IFleetCutoverAcceptedEvidenceReference;
340
+ targetSet: IFleetCutoverTargetSetReference;
341
+ expectedAggregate: IFleetCutoverAcceptedEvidenceReference | null;
342
+ upserts: IFleetCutoverPreparedVolumeHandoffUpsert[];
343
+ removals: IFleetCutoverAcceptedEvidenceReference[];
344
+ }
345
+
346
+ /**
347
+ * Revisioned header for one complete expected set. It intentionally contains no page references:
348
+ * immutable pages bind back to this exact revision, avoiding an aggregate/page digest cycle.
349
+ */
350
+ export interface IFleetCutoverPreparedVolumeHandoffAggregate {
351
+ id: string;
352
+ generation: number;
353
+ previous: IFleetCutoverAcceptedEvidenceReference | null;
354
+ clusterId: string;
355
+ runtime: IClusterRuntimeReference & { phase: 'draining' };
356
+ scope: IFleetCutoverAcceptedEvidenceReference;
357
+ targetSet: IFleetCutoverTargetSetReference;
358
+ state: 'incomplete' | 'complete';
359
+ requiredHandoffCount: number;
360
+ handoffCount: number;
361
+ pageCount: number;
362
+ handoffSetDigest: TSha256Digest;
363
+ acceptedAt: number;
364
+ digest: TSha256Digest;
365
+ }
366
+
367
+ /** One immutable 1..32-entry page pinned to one aggregate revision. */
368
+ export interface IFleetCutoverPreparedVolumeHandoffPage {
369
+ id: string;
370
+ generation: 1;
371
+ aggregate: IFleetCutoverAcceptedEvidenceReference;
372
+ pageIndex: number;
373
+ handoffs: IFleetCutoverPreparedVolumeHandoffPageReference[];
374
+ handoffSetPageDigest: TSha256Digest;
375
+ digest: TSha256Digest;
376
+ }
377
+
378
+ /** Immutable page membership commits both the handoff revision and its owning physical node. */
379
+ export interface IFleetCutoverPreparedVolumeHandoffPageReference
380
+ extends IFleetCutoverAcceptedEvidenceReference {
381
+ nodeId: string;
382
+ }
383
+
384
+ export interface IFleetCutoverPreparedVolumeHandoffPageSetEntry {
385
+ pageIndex: number;
386
+ handoffSetPageDigest: TSha256Digest;
387
+ }
388
+
283
389
  interface IFleetCutoverVolumeFenceBase {
284
390
  id: string;
285
391
  generation: number;
@@ -327,6 +433,40 @@ export interface IFleetCutoverCorestoreAdoptionReceipt {
327
433
  digest: TSha256Digest;
328
434
  }
329
435
 
436
+ /** Current-session Cloudly grant for one post-Pallet adoption attempt. */
437
+ export interface IFleetCutoverStorageAdoptionGrant {
438
+ id: string;
439
+ generation: number;
440
+ previous: IFleetCutoverAcceptedEvidenceReference | null;
441
+ clusterId: string;
442
+ nodeId: string;
443
+ serviceId: string;
444
+ volumeId: string;
445
+ observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
446
+ runtime: IClusterRuntimeReference & { phase: 'pallet' };
447
+ session: IRuntimeSessionBinding;
448
+ handoffIntent: IFleetCutoverAcceptedEvidenceReference;
449
+ palletFence: IFleetCutoverAcceptedEvidenceReference;
450
+ acceptedAt: number;
451
+ digest: TSha256Digest;
452
+ }
453
+
454
+ /** Exact result Pallet persists only after revalidating the current grant following adoption. */
455
+ export interface IFleetCutoverStorageAdoptionBundle {
456
+ grant: IFleetCutoverStorageAdoptionGrant;
457
+ fence: Extract<TFleetCutoverVolumeFence, { state: 'consumed' }>;
458
+ adoption: IFleetCutoverCorestoreAdoptionReceipt;
459
+ claim: IRuntimeStorageClaim;
460
+ }
461
+
462
+ /** One authenticated Spark reporter's complete bounded subset of a full immutable aggregate page. */
463
+ export interface IFleetCutoverHeldVolumeFenceEvidencePage {
464
+ aggregate: IFleetCutoverAcceptedEvidenceReference;
465
+ page: IFleetCutoverPreparedVolumeHandoffPage;
466
+ nodeId: string;
467
+ fences: Extract<TFleetCutoverVolumeFence, { state: 'held' }>[];
468
+ }
469
+
330
470
  export interface IFleetCutoverCoreflowRetirementEvidence {
331
471
  id: string;
332
472
  generation: number;
@@ -373,6 +513,49 @@ export interface IFleetCutoverCoreflowRetirementEvidence {
373
513
  digest: TSha256Digest;
374
514
  }
375
515
 
516
+ /**
517
+ * Administrator intent to prepare the one explicitly designated ingress service. The caller
518
+ * supplies only compare-and-swap inputs; Cloudly owns credential generation and acceptance.
519
+ */
520
+ export interface IFleetCutoverIngressPreparationRequest {
521
+ mutationId: string;
522
+ clusterId: string;
523
+ ingressServiceId: string;
524
+ currentRuntime: IClusterRuntimeReference & { phase: 'pallet' };
525
+ scope: IFleetCutoverAcceptedEvidenceReference;
526
+ targetSet: IFleetCutoverTargetSetReference;
527
+ expectedSpecRevision: number;
528
+ /** Null for creation; the exact current designation for same-service credential rotation. */
529
+ expectedDesignation: IFleetCutoverAcceptedEvidenceReference | null;
530
+ }
531
+
532
+ /** Public identity of the active ingress credential generation; it contains no credential value. */
533
+ export interface IFleetCutoverIngressCredentialReference {
534
+ credentialId: string;
535
+ generation: number;
536
+ secretVersion: ISecretVersionReference;
537
+ }
538
+
539
+ /**
540
+ * Cloudly's accepted pre-assignment designation. This grants no assignment, session,
541
+ * registration, publication, port-transfer, or post-registration ingress authority.
542
+ */
543
+ export interface IFleetCutoverIngressDesignation {
544
+ id: string;
545
+ generation: number;
546
+ previous: IFleetCutoverAcceptedEvidenceReference | null;
547
+ clusterId: string;
548
+ runtime: IClusterRuntimeReference & { phase: 'pallet' };
549
+ scope: IFleetCutoverAcceptedEvidenceReference;
550
+ targetSet: IFleetCutoverTargetSetReference;
551
+ ingressServiceId: string;
552
+ preparedSpecRevision: number;
553
+ credential: IFleetCutoverIngressCredentialReference;
554
+ actorId: string;
555
+ acceptedAt: number;
556
+ digest: TSha256Digest;
557
+ }
558
+
376
559
  export interface IFleetCutoverIngressAuthority {
377
560
  id: string;
378
561
  generation: number;
@@ -453,6 +636,11 @@ export const fleetCutoverContract = Object.freeze({
453
636
  maximumNodes: 1024,
454
637
  maximumContainersPerNode: 512,
455
638
  maximumMountsPerOwner: 32,
639
+ maximumPreparedVolumeHandoffMutationEntries: 32,
640
+ /** Every admitted task has one node placement and at most 32 mounts: 4096 * 32. */
641
+ maximumPreparedVolumeHandoffs: 4096 * 32,
642
+ maximumPreparedVolumeHandoffPages: (4096 * 32) / 32,
643
+ maximumHeldVolumeFenceEvidenceBytes: 131_072,
456
644
  maximumClassifications: 512,
457
645
  observationFreshnessMs: 300_000,
458
646
  mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
@@ -464,9 +652,18 @@ export const fleetCutoverContract = Object.freeze({
464
652
  censusDigestDomain: 'serve.zone/fleet-cutover-census',
465
653
  scopeDigestDomain: 'serve.zone/fleet-cutover-scope',
466
654
  volumeHandoffDigestDomain: 'serve.zone/fleet-cutover-volume-handoff',
655
+ preparedVolumeHandoffDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff',
656
+ preparedVolumeHandoffAggregateDigestDomain:
657
+ 'serve.zone/fleet-cutover-prepared-volume-handoff-aggregate',
658
+ preparedVolumeHandoffPageContentDigestDomain:
659
+ 'serve.zone/fleet-cutover-prepared-volume-handoff-page-content',
660
+ preparedVolumeHandoffPageDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-page',
661
+ preparedVolumeHandoffSetDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-set',
467
662
  volumeFenceDigestDomain: 'serve.zone/fleet-cutover-volume-fence',
468
663
  corestoreAdoptionDigestDomain: 'serve.zone/fleet-cutover-corestore-adoption',
664
+ storageAdoptionGrantDigestDomain: 'serve.zone/fleet-cutover-storage-adoption-grant',
469
665
  coreflowRetirementDigestDomain: 'serve.zone/fleet-cutover-coreflow-retirement',
666
+ ingressDesignationDigestDomain: 'serve.zone/fleet-cutover-ingress-designation',
470
667
  ingressAuthorityDigestDomain: 'serve.zone/fleet-cutover-ingress-authority',
471
668
  legalPhaseTransitions: Object.freeze({
472
669
  swarm: 'draining',
@@ -518,6 +715,20 @@ const assignmentReference = (valueArg: unknown): IRuntimeAssignmentReference =>
518
715
  digest: networkDigest(value.digest),
519
716
  };
520
717
  };
718
+ const ingressCredentialReference = (
719
+ valueArg: unknown,
720
+ ): IFleetCutoverIngressCredentialReference => {
721
+ const value = networkRecord(valueArg, ['credentialId', 'generation', 'secretVersion']);
722
+ const secretVersion = networkRecord(value.secretVersion, ['secretId', 'secretVersionId']);
723
+ return {
724
+ credentialId: networkId(value.credentialId),
725
+ generation: networkCounter(value.generation),
726
+ secretVersion: {
727
+ secretId: networkId(secretVersion.secretId),
728
+ secretVersionId: networkId(secretVersion.secretVersionId),
729
+ },
730
+ };
731
+ };
521
732
  const mountTypeAndClassification = (
522
733
  typeArg: unknown,
523
734
  classificationArg: unknown,
@@ -994,6 +1205,13 @@ export const fleetCutoverEvidenceReferenceOf = (
994
1205
  digest: valueArg.digest,
995
1206
  });
996
1207
 
1208
+ export const fleetCutoverPreparedVolumeHandoffPageReferenceOf = (
1209
+ valueArg: { id: string; generation: number; digest: TSha256Digest; nodeId: string },
1210
+ ): IFleetCutoverPreparedVolumeHandoffPageReference => ({
1211
+ ...fleetCutoverEvidenceReferenceOf(valueArg),
1212
+ nodeId: networkNodeId(valueArg.nodeId),
1213
+ });
1214
+
997
1215
  export const snapshotFleetCutoverAcceptedEvidenceReference = (
998
1216
  valueArg: unknown,
999
1217
  ): IFleetCutoverAcceptedEvidenceReference => safe(() => reference(captureNetwork(valueArg)));
@@ -1150,6 +1368,383 @@ export const computeFleetCutoverVolumeHandoffDigest = async (
1150
1368
  snapshotFleetCutoverVolumeHandoffIntent(valueArg),
1151
1369
  );
1152
1370
 
1371
+ const persistentVolumeMountDescriptor = (
1372
+ valueArg: unknown,
1373
+ ): IFleetCutoverPersistentVolumeMountDescriptor => {
1374
+ const value = mountDescriptor(valueArg);
1375
+ requireNetwork(value.type === 'volume'
1376
+ && value.classification === 'persistent-volume'
1377
+ && value.readOnly === false);
1378
+ return value as IFleetCutoverPersistentVolumeMountDescriptor;
1379
+ };
1380
+
1381
+ const preparedVolumeHandoff = (
1382
+ valueArg: unknown,
1383
+ ): IFleetCutoverPreparedVolumeHandoffIntent => {
1384
+ const value = networkRecord(valueArg, [
1385
+ 'id', 'generation', 'clusterId', 'nodeId', 'serviceId', 'volumeId', 'observedMount',
1386
+ 'claimIntent', 'runtime', 'scope', 'targetSet', 'legacyStorageEvidence',
1387
+ 'sourceNodeObservation', 'acceptedAt', 'digest',
1388
+ ]);
1389
+ const clusterId = networkId(value.clusterId);
1390
+ const nodeId = networkNodeId(value.nodeId);
1391
+ const serviceId = networkId(value.serviceId);
1392
+ const observedMount = persistentVolumeMountDescriptor(value.observedMount);
1393
+ const claimIntent = snapshotRuntimeStorageClaimIntent(value.claimIntent);
1394
+ const runtime = runtimeReference(value.runtime);
1395
+ const targetSet = targetSetReference(value.targetSet);
1396
+ requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
1397
+ && targetSet.cloudlyClusterId === clusterId
1398
+ && claimIntent.clusterId === clusterId
1399
+ && claimIntent.nodeId === nodeId
1400
+ && claimIntent.serviceId === serviceId);
1401
+ return {
1402
+ id: networkId(value.id),
1403
+ generation: networkCounter(value.generation),
1404
+ clusterId,
1405
+ nodeId,
1406
+ serviceId,
1407
+ volumeId: networkId(value.volumeId),
1408
+ observedMount,
1409
+ claimIntent,
1410
+ runtime: runtime as IFleetCutoverPreparedVolumeHandoffIntent['runtime'],
1411
+ scope: reference(value.scope),
1412
+ targetSet,
1413
+ legacyStorageEvidence: reference(value.legacyStorageEvidence),
1414
+ sourceNodeObservation: reference(value.sourceNodeObservation),
1415
+ acceptedAt: networkCounter(value.acceptedAt),
1416
+ digest: networkDigest(value.digest),
1417
+ };
1418
+ };
1419
+
1420
+ export const snapshotFleetCutoverPreparedVolumeHandoffIntent = (
1421
+ valueArg: unknown,
1422
+ ): IFleetCutoverPreparedVolumeHandoffIntent => safe(
1423
+ () => preparedVolumeHandoff(captureNetwork(valueArg)),
1424
+ );
1425
+
1426
+ export const computeFleetCutoverPreparedVolumeHandoffDigest = async (
1427
+ valueArg: unknown,
1428
+ ): Promise<TSha256Digest> => canonicalDigest(
1429
+ fleetCutoverContract.preparedVolumeHandoffDigestDomain,
1430
+ snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg),
1431
+ );
1432
+
1433
+ export const validateFleetCutoverPreparedVolumeHandoffIntent = async (
1434
+ valueArg: unknown,
1435
+ ): Promise<string[]> => {
1436
+ try {
1437
+ const value = snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg);
1438
+ if ((await validateRuntimeStorageClaimIntent(value.claimIntent)).length) {
1439
+ return ['fleet cutover prepared volume handoff claim intent is invalid'];
1440
+ }
1441
+ return value.digest === await computeFleetCutoverPreparedVolumeHandoffDigest(value)
1442
+ ? [] : ['fleet cutover prepared volume handoff digest does not match'];
1443
+ } catch {
1444
+ return ['fleet cutover prepared volume handoff must use its exact canonical schema'];
1445
+ }
1446
+ };
1447
+
1448
+ export const snapshotFleetCutoverPreparedVolumeHandoffMutation = (
1449
+ valueArg: unknown,
1450
+ ): IFleetCutoverPreparedVolumeHandoffMutation => safe(() => {
1451
+ const value = networkRecord(captureNetwork(valueArg), [
1452
+ 'mutationId', 'clusterId', 'currentRuntime', 'scope', 'targetSet', 'expectedAggregate',
1453
+ 'upserts', 'removals',
1454
+ ]);
1455
+ const clusterId = networkId(value.clusterId);
1456
+ const currentRuntime = runtimeReference(value.currentRuntime);
1457
+ const targetSet = targetSetReference(value.targetSet);
1458
+ requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === 'draining'
1459
+ && targetSet.cloudlyClusterId === clusterId);
1460
+ const requestKeys = new Set<string>();
1461
+ const placementKeys = new Set<string>();
1462
+ const upserts = networkSorted(networkList(
1463
+ value.upserts,
1464
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
1465
+ itemArg => {
1466
+ const item = networkRecord(itemArg, [
1467
+ 'nodeId', 'serviceId', 'mountIdentityDigest', 'request',
1468
+ ]);
1469
+ const nodeId = networkNodeId(item.nodeId);
1470
+ const serviceId = networkId(item.serviceId);
1471
+ const mountIdentityDigest = networkDigest(item.mountIdentityDigest);
1472
+ const request = normalizeStorageRequest(item.request);
1473
+ requireNetwork(request.kind === 'filesystem'
1474
+ && request.accessMode === 'ReadWriteOnce'
1475
+ && request.reclaimPolicy === 'retain');
1476
+ const requestKey = `${nodeId}\u0000${serviceId}\u0000${request.id}`;
1477
+ const placementKey = `${nodeId}\u0000${serviceId}\u0000${mountIdentityDigest}`;
1478
+ requireNetwork(!requestKeys.has(requestKey) && !placementKeys.has(placementKey));
1479
+ requestKeys.add(requestKey);
1480
+ placementKeys.add(placementKey);
1481
+ return {
1482
+ nodeId,
1483
+ serviceId,
1484
+ mountIdentityDigest,
1485
+ request: request as IFleetCutoverPreparedVolumeHandoffUpsert['request'],
1486
+ };
1487
+ },
1488
+ ), itemArg => `${itemArg.nodeId}\u0000${itemArg.serviceId}\u0000${itemArg.mountIdentityDigest}`);
1489
+ const removals = networkSorted(networkList(
1490
+ value.removals,
1491
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
1492
+ reference,
1493
+ ), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
1494
+ requireNetwork(upserts.length + removals.length > 0
1495
+ && upserts.length + removals.length
1496
+ <= fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries);
1497
+ return {
1498
+ mutationId: networkId(value.mutationId),
1499
+ clusterId,
1500
+ currentRuntime: currentRuntime as IFleetCutoverPreparedVolumeHandoffMutation['currentRuntime'],
1501
+ scope: reference(value.scope),
1502
+ targetSet,
1503
+ expectedAggregate: value.expectedAggregate === null ? null : reference(value.expectedAggregate),
1504
+ upserts,
1505
+ removals,
1506
+ };
1507
+ });
1508
+
1509
+ const preparedVolumeHandoffAggregate = (
1510
+ valueArg: unknown,
1511
+ ): IFleetCutoverPreparedVolumeHandoffAggregate => {
1512
+ const value = networkRecord(valueArg, [
1513
+ 'id', 'generation', 'previous', 'clusterId', 'runtime', 'scope', 'targetSet', 'state',
1514
+ 'requiredHandoffCount', 'handoffCount', 'pageCount', 'handoffSetDigest', 'acceptedAt',
1515
+ 'digest',
1516
+ ]);
1517
+ const id = networkId(value.id);
1518
+ const generation = networkCounter(value.generation);
1519
+ const previous = value.previous === null ? null : reference(value.previous);
1520
+ requireNetwork(generation === 1
1521
+ ? previous === null
1522
+ : previous?.id === id && previous.generation === generation - 1);
1523
+ const clusterId = networkId(value.clusterId);
1524
+ const runtime = runtimeReference(value.runtime);
1525
+ const targetSet = targetSetReference(value.targetSet);
1526
+ const state = literal(['incomplete', 'complete'] as const, value.state);
1527
+ const requiredHandoffCount = nonNegative(value.requiredHandoffCount);
1528
+ const handoffCount = nonNegative(value.handoffCount);
1529
+ const pageCount = nonNegative(value.pageCount);
1530
+ requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
1531
+ && targetSet.cloudlyClusterId === clusterId
1532
+ && requiredHandoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
1533
+ && handoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
1534
+ && pageCount <= fleetCutoverContract.maximumPreparedVolumeHandoffPages
1535
+ && pageCount === Math.ceil(handoffCount
1536
+ / fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries)
1537
+ && (state === 'complete') === (requiredHandoffCount === handoffCount));
1538
+ return {
1539
+ id,
1540
+ generation,
1541
+ previous,
1542
+ clusterId,
1543
+ runtime: runtime as IFleetCutoverPreparedVolumeHandoffAggregate['runtime'],
1544
+ scope: reference(value.scope),
1545
+ targetSet,
1546
+ state,
1547
+ requiredHandoffCount,
1548
+ handoffCount,
1549
+ pageCount,
1550
+ handoffSetDigest: networkDigest(value.handoffSetDigest),
1551
+ acceptedAt: networkCounter(value.acceptedAt),
1552
+ digest: networkDigest(value.digest),
1553
+ };
1554
+ };
1555
+
1556
+ export const snapshotFleetCutoverPreparedVolumeHandoffAggregate = (
1557
+ valueArg: unknown,
1558
+ ): IFleetCutoverPreparedVolumeHandoffAggregate => safe(
1559
+ () => preparedVolumeHandoffAggregate(captureNetwork(valueArg)),
1560
+ );
1561
+
1562
+ export const computeFleetCutoverPreparedVolumeHandoffAggregateDigest = async (
1563
+ valueArg: unknown,
1564
+ ): Promise<TSha256Digest> => canonicalDigest(
1565
+ fleetCutoverContract.preparedVolumeHandoffAggregateDigestDomain,
1566
+ snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg),
1567
+ );
1568
+
1569
+ export const validateFleetCutoverPreparedVolumeHandoffAggregate = async (
1570
+ valueArg: unknown,
1571
+ ): Promise<string[]> => {
1572
+ try {
1573
+ const value = snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg);
1574
+ return value.digest === await computeFleetCutoverPreparedVolumeHandoffAggregateDigest(value)
1575
+ ? [] : ['fleet cutover prepared volume handoff aggregate digest does not match'];
1576
+ } catch {
1577
+ return ['fleet cutover prepared volume handoff aggregate must use its exact canonical schema'];
1578
+ }
1579
+ };
1580
+
1581
+ const preparedVolumeHandoffPage = (
1582
+ valueArg: unknown,
1583
+ ): IFleetCutoverPreparedVolumeHandoffPage => {
1584
+ const value = networkRecord(valueArg, [
1585
+ 'id', 'generation', 'aggregate', 'pageIndex', 'handoffs', 'handoffSetPageDigest', 'digest',
1586
+ ]);
1587
+ requireNetwork(value.generation === 1);
1588
+ const handoffs = networkSorted(networkList(
1589
+ value.handoffs,
1590
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
1591
+ itemArg => {
1592
+ const item = networkRecord(itemArg, ['id', 'generation', 'digest', 'nodeId']);
1593
+ return fleetCutoverPreparedVolumeHandoffPageReferenceOf({
1594
+ ...reference({ id: item.id, generation: item.generation, digest: item.digest }),
1595
+ nodeId: networkNodeId(item.nodeId),
1596
+ });
1597
+ },
1598
+ ), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
1599
+ const pageIndex = nonNegative(value.pageIndex);
1600
+ requireNetwork(handoffs.length > 0
1601
+ && pageIndex < fleetCutoverContract.maximumPreparedVolumeHandoffPages);
1602
+ return {
1603
+ id: networkId(value.id),
1604
+ generation: 1,
1605
+ aggregate: reference(value.aggregate),
1606
+ pageIndex,
1607
+ handoffs,
1608
+ handoffSetPageDigest: networkDigest(value.handoffSetPageDigest),
1609
+ digest: networkDigest(value.digest),
1610
+ };
1611
+ };
1612
+
1613
+ export const snapshotFleetCutoverPreparedVolumeHandoffPage = (
1614
+ valueArg: unknown,
1615
+ ): IFleetCutoverPreparedVolumeHandoffPage => safe(
1616
+ () => preparedVolumeHandoffPage(captureNetwork(valueArg)),
1617
+ );
1618
+
1619
+ export const computeFleetCutoverPreparedVolumeHandoffPageContentDigest = async (
1620
+ pageIndexArg: unknown,
1621
+ handoffsArg: unknown,
1622
+ ): Promise<TSha256Digest> => {
1623
+ const page = preparedVolumeHandoffPage({
1624
+ id: 'FleetCutoverPreparedVolumeHandoffPage:content',
1625
+ generation: 1,
1626
+ aggregate: { id: 'FleetCutoverPreparedVolumeHandoffAggregate:content', generation: 1,
1627
+ digest: `sha256:${'0'.repeat(64)}` },
1628
+ pageIndex: pageIndexArg,
1629
+ handoffs: handoffsArg,
1630
+ handoffSetPageDigest: `sha256:${'0'.repeat(64)}`,
1631
+ digest: `sha256:${'0'.repeat(64)}`,
1632
+ });
1633
+ return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffPageContentDigestDomain, {
1634
+ pageIndex: page.pageIndex,
1635
+ handoffs: page.handoffs,
1636
+ digest: `sha256:${'0'.repeat(64)}`,
1637
+ });
1638
+ };
1639
+
1640
+ export const computeFleetCutoverPreparedVolumeHandoffPageDigest = async (
1641
+ valueArg: unknown,
1642
+ ): Promise<TSha256Digest> => canonicalDigest(
1643
+ fleetCutoverContract.preparedVolumeHandoffPageDigestDomain,
1644
+ snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg),
1645
+ );
1646
+
1647
+ export const validateFleetCutoverPreparedVolumeHandoffPage = async (
1648
+ valueArg: unknown,
1649
+ ): Promise<string[]> => {
1650
+ try {
1651
+ const value = snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg);
1652
+ if (value.handoffSetPageDigest
1653
+ !== await computeFleetCutoverPreparedVolumeHandoffPageContentDigest(
1654
+ value.pageIndex,
1655
+ value.handoffs,
1656
+ )) {
1657
+ return ['fleet cutover prepared volume handoff page content digest does not match'];
1658
+ }
1659
+ return value.digest === await computeFleetCutoverPreparedVolumeHandoffPageDigest(value)
1660
+ ? [] : ['fleet cutover prepared volume handoff page digest does not match'];
1661
+ } catch {
1662
+ return ['fleet cutover prepared volume handoff page must use its exact canonical schema'];
1663
+ }
1664
+ };
1665
+
1666
+ export const computeFleetCutoverPreparedVolumeHandoffSetDigest = async (
1667
+ entriesArg: unknown,
1668
+ ): Promise<TSha256Digest> => safe(async () => {
1669
+ const entries = networkSorted(networkList(
1670
+ captureNetwork(entriesArg),
1671
+ fleetCutoverContract.maximumPreparedVolumeHandoffPages,
1672
+ itemArg => {
1673
+ const item = networkRecord(itemArg, ['pageIndex', 'handoffSetPageDigest']);
1674
+ return {
1675
+ pageIndex: nonNegative(item.pageIndex),
1676
+ handoffSetPageDigest: networkDigest(item.handoffSetPageDigest),
1677
+ };
1678
+ },
1679
+ ), itemArg => String(itemArg.pageIndex).padStart(8, '0'));
1680
+ requireNetwork(entries.every((entryArg, indexArg) => entryArg.pageIndex === indexArg));
1681
+ return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffSetDigestDomain, {
1682
+ entries,
1683
+ digest: `sha256:${'0'.repeat(64)}`,
1684
+ });
1685
+ });
1686
+
1687
+ /**
1688
+ * Full pinned-set proof. The caller must supply every page for the exact aggregate revision; a
1689
+ * node-local subset or a mixed revision can never satisfy this binder.
1690
+ */
1691
+ export const bindFleetCutoverPreparedVolumeHandoffPages = async (
1692
+ aggregateArg: unknown,
1693
+ pagesArg: unknown,
1694
+ ): Promise<{
1695
+ aggregate: IFleetCutoverPreparedVolumeHandoffAggregate;
1696
+ pages: IFleetCutoverPreparedVolumeHandoffPage[];
1697
+ }> => {
1698
+ const aggregate = snapshotFleetCutoverPreparedVolumeHandoffAggregate(aggregateArg);
1699
+ if ((await validateFleetCutoverPreparedVolumeHandoffAggregate(aggregate)).length) fail();
1700
+ const pages = safe(() => {
1701
+ requireNetwork(Array.isArray(pagesArg)
1702
+ && Object.getPrototypeOf(pagesArg) === Array.prototype
1703
+ && pagesArg.length <= fleetCutoverContract.maximumPreparedVolumeHandoffPages);
1704
+ const keys = Reflect.ownKeys(pagesArg);
1705
+ requireNetwork(keys.length === pagesArg.length + 1);
1706
+ const result: IFleetCutoverPreparedVolumeHandoffPage[] = [];
1707
+ for (let index = 0; index < pagesArg.length; index++) {
1708
+ const descriptor = Object.getOwnPropertyDescriptor(pagesArg, String(index));
1709
+ requireNetwork(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
1710
+ result.push(snapshotFleetCutoverPreparedVolumeHandoffPage(descriptor.value));
1711
+ }
1712
+ return networkSorted(result, itemArg => String(itemArg.pageIndex).padStart(8, '0'));
1713
+ });
1714
+ requireNetwork(pages.length === aggregate.pageCount);
1715
+ let handoffCount = 0;
1716
+ const seen = new Set<string>();
1717
+ let previousHandoffKey: string | null = null;
1718
+ for (const [index, page] of pages.entries()) {
1719
+ requireNetwork(page.pageIndex === index
1720
+ && page.aggregate.id === aggregate.id
1721
+ && page.aggregate.generation === aggregate.generation
1722
+ && page.aggregate.digest === aggregate.digest
1723
+ && !(await validateFleetCutoverPreparedVolumeHandoffPage(page)).length);
1724
+ const expectedLength = index < pages.length - 1
1725
+ ? fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
1726
+ : aggregate.handoffCount
1727
+ % fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
1728
+ || fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries;
1729
+ requireNetwork(page.handoffs.length === expectedLength);
1730
+ handoffCount += page.handoffs.length;
1731
+ for (const handoff of page.handoffs) {
1732
+ const key = `${handoff.id}\u0000${handoff.generation}\u0000${handoff.digest}`;
1733
+ requireNetwork(!seen.has(key) && (previousHandoffKey === null || previousHandoffKey < key));
1734
+ seen.add(key);
1735
+ previousHandoffKey = key;
1736
+ }
1737
+ }
1738
+ const entries = pages.map(page => ({
1739
+ pageIndex: page.pageIndex,
1740
+ handoffSetPageDigest: page.handoffSetPageDigest,
1741
+ }));
1742
+ requireNetwork(handoffCount === aggregate.handoffCount
1743
+ && aggregate.handoffSetDigest
1744
+ === await computeFleetCutoverPreparedVolumeHandoffSetDigest(entries));
1745
+ return { aggregate, pages };
1746
+ };
1747
+
1153
1748
  const volumeFence = (valueArg: unknown): TFleetCutoverVolumeFence => {
1154
1749
  requireNetwork(valueArg !== null && typeof valueArg === 'object');
1155
1750
  const state = (valueArg as Record<string, unknown>).state;
@@ -1265,6 +1860,45 @@ export const bindFleetCutoverAdoptionToVolumeFence = (
1265
1860
  return { adoption, fence };
1266
1861
  });
1267
1862
 
1863
+ /**
1864
+ * Joins the accepted adoption result to the full handoff and provider-ready claim content.
1865
+ * Native attachment fencing and accepted-record dereferencing remain owner checks.
1866
+ */
1867
+ export const bindFleetCutoverAdoptionToStorageClaim = async (
1868
+ adoptionArg: unknown,
1869
+ handoffArg: unknown,
1870
+ claimArg: unknown,
1871
+ ): Promise<{
1872
+ adoption: IFleetCutoverCorestoreAdoptionReceipt;
1873
+ handoff: IFleetCutoverPreparedVolumeHandoffIntent;
1874
+ claim: IRuntimeStorageClaim;
1875
+ }> => {
1876
+ try {
1877
+ const adoption = snapshotFleetCutoverCorestoreAdoptionReceipt(adoptionArg);
1878
+ const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
1879
+ const claim = snapshotRuntimeStorageClaim(claimArg);
1880
+ if ((await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
1881
+ || (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
1882
+ || (await validateRuntimeStorageClaim(claim)).length) fail();
1883
+ await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
1884
+ requireNetwork(adoption.clusterId === handoff.clusterId
1885
+ && adoption.nodeId === handoff.nodeId
1886
+ && adoption.serviceId === handoff.serviceId
1887
+ && adoption.volumeId === handoff.volumeId
1888
+ && adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
1889
+ && adoption.handoffIntent.id === handoff.id
1890
+ && adoption.handoffIntent.generation === handoff.generation
1891
+ && adoption.handoffIntent.digest === handoff.digest
1892
+ && adoption.claim.id === claim.id
1893
+ && adoption.claim.generation === claim.generation
1894
+ && adoption.claim.digest === claim.digest
1895
+ && adoption.acceptedAt >= handoff.acceptedAt);
1896
+ return { adoption, handoff, claim };
1897
+ } catch {
1898
+ return fail();
1899
+ }
1900
+ };
1901
+
1268
1902
  export const validateFleetCutoverVolumeHandoffIntent = async (valueArg: unknown): Promise<string[]> => {
1269
1903
  try {
1270
1904
  const value = snapshotFleetCutoverVolumeHandoffIntent(valueArg);
@@ -1314,6 +1948,273 @@ const runtimeSession = (valueArg: unknown): IRuntimeSessionBinding => {
1314
1948
  };
1315
1949
  };
1316
1950
 
1951
+ const storageAdoptionGrant = (valueArg: unknown): IFleetCutoverStorageAdoptionGrant => {
1952
+ const value = networkRecord(valueArg, [
1953
+ 'id', 'generation', 'previous', 'clusterId', 'nodeId', 'serviceId', 'volumeId',
1954
+ 'observedMount', 'runtime', 'session', 'handoffIntent', 'palletFence', 'acceptedAt',
1955
+ 'digest',
1956
+ ]);
1957
+ const id = networkId(value.id);
1958
+ const generation = networkCounter(value.generation);
1959
+ const previous = value.previous === null ? null : reference(value.previous);
1960
+ requireNetwork(generation === 1
1961
+ ? previous === null
1962
+ : previous?.id === id && previous.generation === generation - 1);
1963
+ const clusterId = networkId(value.clusterId);
1964
+ const nodeId = networkNodeId(value.nodeId);
1965
+ const runtime = runtimeReference(value.runtime);
1966
+ const session = runtimeSession(value.session);
1967
+ requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
1968
+ && session.clusterId === clusterId
1969
+ && session.nodeId === nodeId
1970
+ && session.controller.kind === 'cloudly');
1971
+ return {
1972
+ id,
1973
+ generation,
1974
+ previous,
1975
+ clusterId,
1976
+ nodeId,
1977
+ serviceId: networkId(value.serviceId),
1978
+ volumeId: networkId(value.volumeId),
1979
+ observedMount: persistentVolumeMountDescriptor(value.observedMount),
1980
+ runtime: runtime as IFleetCutoverStorageAdoptionGrant['runtime'],
1981
+ session,
1982
+ handoffIntent: reference(value.handoffIntent),
1983
+ palletFence: reference(value.palletFence),
1984
+ acceptedAt: networkCounter(value.acceptedAt),
1985
+ digest: networkDigest(value.digest),
1986
+ };
1987
+ };
1988
+
1989
+ export const snapshotFleetCutoverStorageAdoptionGrant = (
1990
+ valueArg: unknown,
1991
+ ): IFleetCutoverStorageAdoptionGrant => safe(
1992
+ () => storageAdoptionGrant(captureNetwork(valueArg)),
1993
+ );
1994
+
1995
+ export const computeFleetCutoverStorageAdoptionGrantDigest = async (
1996
+ valueArg: unknown,
1997
+ ): Promise<TSha256Digest> => canonicalDigest(
1998
+ fleetCutoverContract.storageAdoptionGrantDigestDomain,
1999
+ snapshotFleetCutoverStorageAdoptionGrant(valueArg),
2000
+ );
2001
+
2002
+ export const validateFleetCutoverStorageAdoptionGrant = async (
2003
+ valueArg: unknown,
2004
+ ): Promise<string[]> => {
2005
+ try {
2006
+ const value = snapshotFleetCutoverStorageAdoptionGrant(valueArg);
2007
+ return value.digest === await computeFleetCutoverStorageAdoptionGrantDigest(value)
2008
+ ? [] : ['fleet cutover storage adoption grant digest does not match'];
2009
+ } catch {
2010
+ return ['fleet cutover storage adoption grant must use its exact canonical schema'];
2011
+ }
2012
+ };
2013
+
2014
+ /** Content join only; the controller must still prove this is the live current session and phase. */
2015
+ export const bindFleetCutoverStorageAdoptionGrant = async (
2016
+ grantArg: unknown,
2017
+ handoffArg: unknown,
2018
+ heldFenceArg: unknown,
2019
+ ): Promise<{
2020
+ grant: IFleetCutoverStorageAdoptionGrant;
2021
+ handoff: IFleetCutoverPreparedVolumeHandoffIntent;
2022
+ fence: Extract<TFleetCutoverVolumeFence, { state: 'held' }>;
2023
+ }> => {
2024
+ try {
2025
+ const grant = snapshotFleetCutoverStorageAdoptionGrant(grantArg);
2026
+ const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
2027
+ const fence = snapshotFleetCutoverVolumeFence(heldFenceArg);
2028
+ if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
2029
+ || (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
2030
+ || (await validateFleetCutoverVolumeFence(fence)).length) fail();
2031
+ requireNetwork(fence.state === 'held'
2032
+ && grant.clusterId === handoff.clusterId
2033
+ && grant.nodeId === handoff.nodeId
2034
+ && grant.serviceId === handoff.serviceId
2035
+ && grant.volumeId === handoff.volumeId
2036
+ && grant.observedMount.type === handoff.observedMount.type
2037
+ && grant.observedMount.classification === handoff.observedMount.classification
2038
+ && grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
2039
+ && grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
2040
+ && grant.observedMount.readOnly === handoff.observedMount.readOnly
2041
+ && grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2042
+ && grant.handoffIntent.id === handoff.id
2043
+ && grant.handoffIntent.generation === handoff.generation
2044
+ && grant.handoffIntent.digest === handoff.digest
2045
+ && grant.palletFence.id === fence.id
2046
+ && grant.palletFence.generation === fence.generation
2047
+ && grant.palletFence.digest === fence.digest
2048
+ && fence.clusterId === handoff.clusterId
2049
+ && fence.nodeId === handoff.nodeId
2050
+ && fence.serviceId === handoff.serviceId
2051
+ && fence.volumeId === handoff.volumeId
2052
+ && fence.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2053
+ && fence.handoffIntent.id === handoff.id
2054
+ && fence.handoffIntent.generation === handoff.generation
2055
+ && fence.handoffIntent.digest === handoff.digest
2056
+ && fence.heldRuntime.id === handoff.runtime.id
2057
+ && fence.heldRuntime.phase === handoff.runtime.phase
2058
+ && fence.heldRuntime.generation === handoff.runtime.generation
2059
+ && grant.session.controller.id === handoff.claimIntent.controller.id
2060
+ && grant.session.controller.epoch === handoff.claimIntent.controller.epoch
2061
+ && grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
2062
+ && grant.acceptedAt >= fence.heldAt);
2063
+ return { grant, handoff, fence };
2064
+ } catch {
2065
+ return fail();
2066
+ }
2067
+ };
2068
+
2069
+ export const snapshotFleetCutoverStorageAdoptionBundle = (
2070
+ valueArg: unknown,
2071
+ ): IFleetCutoverStorageAdoptionBundle => safe(() => {
2072
+ const value = networkRecord(captureNetwork(valueArg), [
2073
+ 'grant', 'fence', 'adoption', 'claim',
2074
+ ]);
2075
+ const fence = snapshotFleetCutoverVolumeFence(value.fence);
2076
+ requireNetwork(fence.state === 'consumed');
2077
+ return {
2078
+ grant: snapshotFleetCutoverStorageAdoptionGrant(value.grant),
2079
+ fence,
2080
+ adoption: snapshotFleetCutoverCorestoreAdoptionReceipt(value.adoption),
2081
+ claim: snapshotRuntimeStorageClaim(value.claim),
2082
+ };
2083
+ });
2084
+
2085
+ /**
2086
+ * Binds the complete post-adoption content. Physical lineage remains the provider-produced
2087
+ * adoption digest and is deliberately never equated to the observed Docker source identity.
2088
+ */
2089
+ export const bindFleetCutoverStorageAdoptionBundle = async (
2090
+ valueArg: unknown,
2091
+ handoffArg: unknown,
2092
+ ): Promise<{
2093
+ bundle: IFleetCutoverStorageAdoptionBundle;
2094
+ handoff: IFleetCutoverPreparedVolumeHandoffIntent;
2095
+ }> => {
2096
+ try {
2097
+ const bundle = snapshotFleetCutoverStorageAdoptionBundle(valueArg);
2098
+ const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
2099
+ const { grant, fence, adoption, claim } = bundle;
2100
+ if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
2101
+ || (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
2102
+ || (await validateFleetCutoverVolumeFence(fence)).length
2103
+ || (await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
2104
+ || (await validateRuntimeStorageClaim(claim)).length) fail();
2105
+ await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
2106
+ requireNetwork(grant.clusterId === handoff.clusterId
2107
+ && grant.nodeId === handoff.nodeId
2108
+ && grant.serviceId === handoff.serviceId
2109
+ && grant.volumeId === handoff.volumeId
2110
+ && grant.observedMount.type === handoff.observedMount.type
2111
+ && grant.observedMount.classification === handoff.observedMount.classification
2112
+ && grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
2113
+ && grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
2114
+ && grant.observedMount.readOnly === handoff.observedMount.readOnly
2115
+ && grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2116
+ && grant.session.controller.id === handoff.claimIntent.controller.id
2117
+ && grant.session.controller.epoch === handoff.claimIntent.controller.epoch
2118
+ && grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
2119
+ && grant.handoffIntent.id === handoff.id
2120
+ && grant.handoffIntent.generation === handoff.generation
2121
+ && grant.handoffIntent.digest === handoff.digest
2122
+ && fence.clusterId === grant.clusterId
2123
+ && fence.nodeId === grant.nodeId
2124
+ && fence.serviceId === grant.serviceId
2125
+ && fence.volumeId === grant.volumeId
2126
+ && fence.mountIdentityDigest === grant.observedMount.mountIdentityDigest
2127
+ && fence.handoffIntent.id === handoff.id
2128
+ && fence.handoffIntent.generation === handoff.generation
2129
+ && fence.handoffIntent.digest === handoff.digest
2130
+ && fence.previous !== null
2131
+ && fence.previous.id === grant.palletFence.id
2132
+ && fence.previous.generation === grant.palletFence.generation
2133
+ && fence.previous.digest === grant.palletFence.digest
2134
+ && fence.consumedRuntime.id === grant.runtime.id
2135
+ && fence.consumedRuntime.phase === grant.runtime.phase
2136
+ && fence.consumedRuntime.generation === grant.runtime.generation
2137
+ && adoption.clusterId === handoff.clusterId
2138
+ && adoption.nodeId === handoff.nodeId
2139
+ && adoption.serviceId === handoff.serviceId
2140
+ && adoption.volumeId === handoff.volumeId
2141
+ && adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2142
+ && adoption.handoffIntent.id === handoff.id
2143
+ && adoption.handoffIntent.generation === handoff.generation
2144
+ && adoption.handoffIntent.digest === handoff.digest
2145
+ && adoption.palletFence.id === grant.palletFence.id
2146
+ && adoption.palletFence.generation === grant.palletFence.generation
2147
+ && adoption.palletFence.digest === grant.palletFence.digest
2148
+ && adoption.claim.id === claim.id
2149
+ && adoption.claim.generation === claim.generation
2150
+ && adoption.claim.digest === claim.digest
2151
+ && fence.adoptionReceipt.id === adoption.id
2152
+ && fence.adoptionReceipt.generation === adoption.generation
2153
+ && fence.adoptionReceipt.digest === adoption.digest
2154
+ && fence.claim.id === claim.id
2155
+ && fence.claim.generation === claim.generation
2156
+ && fence.claim.digest === claim.digest
2157
+ && adoption.acceptedAt >= grant.acceptedAt
2158
+ && fence.consumedAt >= adoption.acceptedAt);
2159
+ return { bundle, handoff };
2160
+ } catch {
2161
+ return fail();
2162
+ }
2163
+ };
2164
+
2165
+ export const snapshotFleetCutoverHeldVolumeFenceEvidencePage = (
2166
+ valueArg: unknown,
2167
+ ): IFleetCutoverHeldVolumeFenceEvidencePage => safe(() => {
2168
+ const value = networkRecord(captureNetwork(valueArg), ['aggregate', 'page', 'nodeId', 'fences']);
2169
+ const aggregate = reference(value.aggregate);
2170
+ const page = preparedVolumeHandoffPage(value.page);
2171
+ const nodeId = networkNodeId(value.nodeId);
2172
+ requireNetwork(page.aggregate.id === aggregate.id
2173
+ && page.aggregate.generation === aggregate.generation
2174
+ && page.aggregate.digest === aggregate.digest);
2175
+ const pageHandoffs = page.handoffs.filter(itemArg => itemArg.nodeId === nodeId);
2176
+ const fences = networkSorted(networkList(
2177
+ value.fences,
2178
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
2179
+ itemArg => {
2180
+ const fence = volumeFence(itemArg);
2181
+ requireNetwork(fence.state === 'held' && fence.nodeId === nodeId);
2182
+ return fence;
2183
+ },
2184
+ ), itemArg => `${itemArg.handoffIntent.id}\u0000${itemArg.handoffIntent.generation}\u0000${itemArg.handoffIntent.digest}`);
2185
+ requireNetwork(fences.length === pageHandoffs.length
2186
+ && fences.every((fenceArg, indexArg) => {
2187
+ const handoff = pageHandoffs[indexArg];
2188
+ return fenceArg.handoffIntent.id === handoff.id
2189
+ && fenceArg.handoffIntent.generation === handoff.generation
2190
+ && fenceArg.handoffIntent.digest === handoff.digest;
2191
+ }));
2192
+ return { aggregate, page, nodeId, fences };
2193
+ });
2194
+
2195
+ export const validateFleetCutoverHeldVolumeFenceEvidencePage = async (
2196
+ valueArg: unknown,
2197
+ ): Promise<string[]> => {
2198
+ try {
2199
+ const value = snapshotFleetCutoverHeldVolumeFenceEvidencePage(valueArg);
2200
+ if (new TextEncoder().encode(canonicalizeStrictJson(value, fail)).byteLength
2201
+ > fleetCutoverContract.maximumHeldVolumeFenceEvidenceBytes) {
2202
+ return ['fleet cutover held volume fence evidence page exceeds its encoded byte bound'];
2203
+ }
2204
+ if ((await validateFleetCutoverPreparedVolumeHandoffPage(value.page)).length) {
2205
+ return ['fleet cutover held volume fence evidence page is not a valid immutable page'];
2206
+ }
2207
+ for (const fence of value.fences) {
2208
+ if ((await validateFleetCutoverVolumeFence(fence)).length) {
2209
+ return ['fleet cutover held volume fence evidence contains an invalid held fence'];
2210
+ }
2211
+ }
2212
+ return [];
2213
+ } catch {
2214
+ return ['fleet cutover held volume fence evidence page must use its exact canonical schema'];
2215
+ }
2216
+ };
2217
+
1317
2218
  export const snapshotFleetCutoverCoreflowRetirementEvidence = (
1318
2219
  valueArg: unknown,
1319
2220
  ): IFleetCutoverCoreflowRetirementEvidence => safe(() => {
@@ -1418,6 +2319,83 @@ export const validateFleetCutoverCoreflowRetirementEvidence = async (
1418
2319
  } catch { return ['fleet cutover Coreflow retirement evidence must use its exact canonical schema']; }
1419
2320
  };
1420
2321
 
2322
+ export const snapshotFleetCutoverIngressPreparationRequest = (
2323
+ valueArg: unknown,
2324
+ ): IFleetCutoverIngressPreparationRequest => safe(() => {
2325
+ const value = networkRecord(captureNetwork(valueArg), [
2326
+ 'mutationId', 'clusterId', 'ingressServiceId', 'currentRuntime', 'scope', 'targetSet',
2327
+ 'expectedSpecRevision', 'expectedDesignation',
2328
+ ]);
2329
+ const clusterId = networkId(value.clusterId);
2330
+ const currentRuntime = runtimeReference(value.currentRuntime);
2331
+ const targetSet = targetSetReference(value.targetSet);
2332
+ requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === 'pallet'
2333
+ && targetSet.cloudlyClusterId === clusterId);
2334
+ return {
2335
+ mutationId: networkId(value.mutationId),
2336
+ clusterId,
2337
+ ingressServiceId: networkId(value.ingressServiceId),
2338
+ currentRuntime: currentRuntime as IFleetCutoverIngressPreparationRequest['currentRuntime'],
2339
+ scope: reference(value.scope),
2340
+ targetSet,
2341
+ expectedSpecRevision: networkCounter(value.expectedSpecRevision),
2342
+ expectedDesignation: value.expectedDesignation === null
2343
+ ? null : reference(value.expectedDesignation),
2344
+ };
2345
+ });
2346
+
2347
+ export const snapshotFleetCutoverIngressDesignation = (
2348
+ valueArg: unknown,
2349
+ ): IFleetCutoverIngressDesignation => safe(() => {
2350
+ const value = networkRecord(captureNetwork(valueArg), [
2351
+ 'id', 'generation', 'previous', 'clusterId', 'runtime', 'scope', 'targetSet',
2352
+ 'ingressServiceId', 'preparedSpecRevision', 'credential', 'actorId', 'acceptedAt', 'digest',
2353
+ ]);
2354
+ const id = networkId(value.id);
2355
+ const generation = networkCounter(value.generation);
2356
+ const previous = value.previous === null ? null : reference(value.previous);
2357
+ const clusterId = networkId(value.clusterId);
2358
+ const runtime = runtimeReference(value.runtime);
2359
+ const targetSet = targetSetReference(value.targetSet);
2360
+ requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
2361
+ && targetSet.cloudlyClusterId === clusterId
2362
+ && (generation === 1
2363
+ ? previous === null
2364
+ : previous?.id === id && previous.generation === generation - 1));
2365
+ return {
2366
+ id,
2367
+ generation,
2368
+ previous,
2369
+ clusterId,
2370
+ runtime: runtime as IFleetCutoverIngressDesignation['runtime'],
2371
+ scope: reference(value.scope),
2372
+ targetSet,
2373
+ ingressServiceId: networkId(value.ingressServiceId),
2374
+ preparedSpecRevision: networkCounter(value.preparedSpecRevision),
2375
+ credential: ingressCredentialReference(value.credential),
2376
+ actorId: networkId(value.actorId),
2377
+ acceptedAt: networkCounter(value.acceptedAt),
2378
+ digest: networkDigest(value.digest),
2379
+ };
2380
+ });
2381
+
2382
+ export const computeFleetCutoverIngressDesignationDigest = async (
2383
+ valueArg: unknown,
2384
+ ): Promise<TSha256Digest> => canonicalDigest(
2385
+ fleetCutoverContract.ingressDesignationDigestDomain,
2386
+ snapshotFleetCutoverIngressDesignation(valueArg),
2387
+ );
2388
+
2389
+ export const validateFleetCutoverIngressDesignation = async (
2390
+ valueArg: unknown,
2391
+ ): Promise<string[]> => {
2392
+ try {
2393
+ const value = snapshotFleetCutoverIngressDesignation(valueArg);
2394
+ return value.digest === await computeFleetCutoverIngressDesignationDigest(value)
2395
+ ? [] : ['fleet cutover ingress designation digest does not match'];
2396
+ } catch { return ['fleet cutover ingress designation must use its exact canonical schema']; }
2397
+ };
2398
+
1421
2399
  export const snapshotFleetCutoverIngressAuthority = (
1422
2400
  valueArg: unknown,
1423
2401
  ): IFleetCutoverIngressAuthority => safe(() => {