@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.
@@ -6,6 +6,16 @@ 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';
9
19
  import type { ISecretVersionReference } from './secret.js';
10
20
  import {
11
21
  canonicalizeStrictJson,
@@ -281,6 +291,101 @@ export interface IFleetCutoverVolumeHandoffIntent {
281
291
  digest: TSha256Digest;
282
292
  }
283
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
+
284
389
  interface IFleetCutoverVolumeFenceBase {
285
390
  id: string;
286
391
  generation: number;
@@ -328,6 +433,40 @@ export interface IFleetCutoverCorestoreAdoptionReceipt {
328
433
  digest: TSha256Digest;
329
434
  }
330
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
+
331
470
  export interface IFleetCutoverCoreflowRetirementEvidence {
332
471
  id: string;
333
472
  generation: number;
@@ -497,6 +636,11 @@ export const fleetCutoverContract = Object.freeze({
497
636
  maximumNodes: 1024,
498
637
  maximumContainersPerNode: 512,
499
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,
500
644
  maximumClassifications: 512,
501
645
  observationFreshnessMs: 300_000,
502
646
  mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
@@ -508,8 +652,16 @@ export const fleetCutoverContract = Object.freeze({
508
652
  censusDigestDomain: 'serve.zone/fleet-cutover-census',
509
653
  scopeDigestDomain: 'serve.zone/fleet-cutover-scope',
510
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',
511
662
  volumeFenceDigestDomain: 'serve.zone/fleet-cutover-volume-fence',
512
663
  corestoreAdoptionDigestDomain: 'serve.zone/fleet-cutover-corestore-adoption',
664
+ storageAdoptionGrantDigestDomain: 'serve.zone/fleet-cutover-storage-adoption-grant',
513
665
  coreflowRetirementDigestDomain: 'serve.zone/fleet-cutover-coreflow-retirement',
514
666
  ingressDesignationDigestDomain: 'serve.zone/fleet-cutover-ingress-designation',
515
667
  ingressAuthorityDigestDomain: 'serve.zone/fleet-cutover-ingress-authority',
@@ -1053,6 +1205,13 @@ export const fleetCutoverEvidenceReferenceOf = (
1053
1205
  digest: valueArg.digest,
1054
1206
  });
1055
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
+
1056
1215
  export const snapshotFleetCutoverAcceptedEvidenceReference = (
1057
1216
  valueArg: unknown,
1058
1217
  ): IFleetCutoverAcceptedEvidenceReference => safe(() => reference(captureNetwork(valueArg)));
@@ -1209,6 +1368,383 @@ export const computeFleetCutoverVolumeHandoffDigest = async (
1209
1368
  snapshotFleetCutoverVolumeHandoffIntent(valueArg),
1210
1369
  );
1211
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
+
1212
1748
  const volumeFence = (valueArg: unknown): TFleetCutoverVolumeFence => {
1213
1749
  requireNetwork(valueArg !== null && typeof valueArg === 'object');
1214
1750
  const state = (valueArg as Record<string, unknown>).state;
@@ -1324,6 +1860,45 @@ export const bindFleetCutoverAdoptionToVolumeFence = (
1324
1860
  return { adoption, fence };
1325
1861
  });
1326
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
+
1327
1902
  export const validateFleetCutoverVolumeHandoffIntent = async (valueArg: unknown): Promise<string[]> => {
1328
1903
  try {
1329
1904
  const value = snapshotFleetCutoverVolumeHandoffIntent(valueArg);
@@ -1373,6 +1948,273 @@ const runtimeSession = (valueArg: unknown): IRuntimeSessionBinding => {
1373
1948
  };
1374
1949
  };
1375
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
+
1376
2218
  export const snapshotFleetCutoverCoreflowRetirementEvidence = (
1377
2219
  valueArg: unknown,
1378
2220
  ): IFleetCutoverCoreflowRetirementEvidence => safe(() => {