@serve.zone/interfaces 32.21.0 → 32.23.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.
Files changed (56) hide show
  1. package/changelog.md +91 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/data/clusteringress.d.ts +58 -0
  4. package/dist_ts/data/clusteringress.js +96 -2
  5. package/dist_ts/data/clusteringressdesignation.d.ts +64 -0
  6. package/dist_ts/data/clusteringressdesignation.js +84 -0
  7. package/dist_ts/data/corestorelegacyadoption.d.ts +178 -0
  8. package/dist_ts/data/corestorelegacyadoption.js +289 -0
  9. package/dist_ts/data/index.d.ts +6 -0
  10. package/dist_ts/data/index.js +6 -1
  11. package/dist_ts/data/networkprojection.d.ts +18 -0
  12. package/dist_ts/data/networkprojection.js +31 -2
  13. package/dist_ts/data/networkprotection.d.ts +13 -0
  14. package/dist_ts/data/networkprotection.js +8 -2
  15. package/dist_ts/data/networkworkloadingress.d.ts +61 -0
  16. package/dist_ts/data/networkworkloadingress.js +54 -0
  17. package/dist_ts/data/runtimelocalstorageclaim.d.ts +124 -0
  18. package/dist_ts/data/runtimelocalstorageclaim.js +167 -0
  19. package/dist_ts/data/service.d.ts +15 -1
  20. package/dist_ts/data/service.js +1 -1
  21. package/dist_ts/data/serviceruntime.d.ts +15 -3
  22. package/dist_ts/data/serviceruntime.js +7 -2
  23. package/dist_ts/data/servicevolumeinitialization.d.ts +91 -0
  24. package/dist_ts/data/servicevolumeinitialization.js +93 -0
  25. package/dist_ts/platform/types.d.ts +45 -0
  26. package/dist_ts/platform/types.js +27 -2
  27. package/dist_ts/private/storagecontract.d.ts +1 -0
  28. package/dist_ts/private/storagecontract.js +2 -1
  29. package/dist_ts/requests/clusteringress.d.ts +52 -0
  30. package/dist_ts/requests/clusteringress.js +1 -1
  31. package/dist_ts/requests/corestore.d.ts +60 -0
  32. package/dist_ts/requests/corestore.js +1 -1
  33. package/dist_ts/requests/platform.d.ts +6 -1
  34. package/dist_ts/requests/runtimesession.d.ts +12 -0
  35. package/dist_ts/requests/service.d.ts +23 -0
  36. package/package.json +1 -1
  37. package/readme.md +262 -2
  38. package/ts/00_commitinfo_data.ts +1 -1
  39. package/ts/data/clusteringress.ts +136 -1
  40. package/ts/data/clusteringressdesignation.ts +141 -0
  41. package/ts/data/corestorelegacyadoption.ts +471 -0
  42. package/ts/data/index.ts +7 -0
  43. package/ts/data/networkprojection.ts +49 -1
  44. package/ts/data/networkprotection.ts +20 -1
  45. package/ts/data/networkworkloadingress.ts +107 -0
  46. package/ts/data/runtimelocalstorageclaim.ts +275 -0
  47. package/ts/data/service.ts +16 -0
  48. package/ts/data/serviceruntime.ts +19 -2
  49. package/ts/data/servicevolumeinitialization.ts +159 -0
  50. package/ts/platform/types.ts +75 -0
  51. package/ts/private/storagecontract.ts +1 -0
  52. package/ts/requests/clusteringress.ts +65 -0
  53. package/ts/requests/corestore.ts +79 -0
  54. package/ts/requests/platform.ts +6 -0
  55. package/ts/requests/runtimesession.ts +16 -0
  56. package/ts/requests/service.ts +28 -0
@@ -42,6 +42,18 @@ export interface IReportRuntimeNetworkProtectionRequest {
42
42
  * Absent from a node that has not stated one; never defaulted, never guessed.
43
43
  */
44
44
  uplinkAddress?: string;
45
+ /**
46
+ * Every IPv4 address the node's host carries on interfaces the node does not manage itself, as
47
+ * the node observed them: sorted as strings, unique, never empty when present, at most
48
+ * `runtimeNetworkProtectionContract.maximumHostAddresses`, and each one an address
49
+ * `runtimeNodeUplinkAddress` admits. Workload bridges and the managed VPN interface are the
50
+ * node's own and are left out.
51
+ *
52
+ * Cloudly names a platform endpoint in the node's `hostPlatformEndpointIds` when its address is
53
+ * one of these, which is how a host without a relay registration (a Corestore beside the node on
54
+ * one machine) states that an endpoint is local. Absent states nothing; never defaulted.
55
+ */
56
+ hostAddresses?: string[];
45
57
  }
46
58
  export interface IReportRuntimeNetworkProtectionResponse {
47
59
  status: 'accepted' | 'replay';
@@ -57,6 +69,7 @@ export const runtimeNetworkProtectionContract = Object.freeze({
57
69
  unavailable: 'null-native-barrier-removes-allocation-eligibility',
58
70
  historical: 'retain-for-outbox-replay-never-current-allocation-eligibility',
59
71
  quarantine: 'no-release-authority',
72
+ maximumHostAddresses: 32,
60
73
  } as const);
61
74
 
62
75
  const scopeOf = (value: IRuntimeNetworkNodeScope): IRuntimeNetworkNodeScope =>
@@ -123,7 +136,7 @@ export const admitRuntimeNetworkProtectionReceipt = async (
123
136
  };
124
137
 
125
138
  export const snapshotReportRuntimeNetworkProtectionRequest = (value: unknown): IReportRuntimeNetworkProtectionRequest => networkSafe(() => {
126
- const item = networkRecord(captureNetwork(value), ['session', 'receipt'], ['uplinkAddress']);
139
+ const item = networkRecord(captureNetwork(value), ['session', 'receipt'], ['uplinkAddress', 'hostAddresses']);
127
140
  const request: IReportRuntimeNetworkProtectionRequest = {
128
141
  session: snapshotRuntimeSessionBinding(item.session),
129
142
  receipt: snapshotRuntimeNetworkProtectionReceipt(item.receipt),
@@ -131,6 +144,12 @@ export const snapshotReportRuntimeNetworkProtectionRequest = (value: unknown): I
131
144
  if (Object.hasOwn(item, 'uplinkAddress')) {
132
145
  request.uplinkAddress = runtimeNodeUplinkAddress(item.uplinkAddress);
133
146
  }
147
+ if (Object.hasOwn(item, 'hostAddresses')) {
148
+ const hostAddresses = networkSorted(networkList(item.hostAddresses,
149
+ runtimeNetworkProtectionContract.maximumHostAddresses, runtimeNodeUplinkAddress), (address) => address);
150
+ requireNetwork(hostAddresses.length > 0);
151
+ request.hostAddresses = hostAddresses;
152
+ }
134
153
  return request;
135
154
  });
136
155
 
@@ -0,0 +1,107 @@
1
+ import type { IRuntimeAuthorityReference, IRuntimeController } from './runtimeassignment.js';
2
+ import type { IRuntimeNetworkWorkloadLease } from './networkprojection.js';
3
+ import { networkCanonical, networkCounter, networkEqual, networkList, networkRecord, networkReference,
4
+ networkReferenceOf, networkSameScope, networkSorted, requireNetwork } from '../private/networkcontract.js';
5
+
6
+ /**
7
+ * Exact one-way flow authority from one workload to one port of another: the cluster ingress
8
+ * workload dials the target workload's lease address on exactly this protocol and port.
9
+ *
10
+ * Unlike the projection's workload grants it needs neither the same organization nor a shared
11
+ * private network, and it opens one port instead of every port; nothing flows the other way except
12
+ * the replies of a connection the source opened.
13
+ */
14
+ export interface IRuntimeNetworkWorkloadIngressPacketGrant {
15
+ source: IRuntimeAuthorityReference;
16
+ destination: { lease: IRuntimeAuthorityReference; protocol: 'tcp' | 'udp'; port: number };
17
+ }
18
+
19
+ /** One target port the ingress workload may dial, and the Cloudly decision that authorized it. */
20
+ export interface IRuntimeNetworkWorkloadIngress {
21
+ /** Exact lease of the ingress workload, an endpoint of this projection. */
22
+ source: IRuntimeAuthorityReference;
23
+ /** Exact lease of the target workload, another endpoint of this projection. */
24
+ destination: IRuntimeAuthorityReference;
25
+ protocol: 'tcp' | 'udp';
26
+ /** The target workload's own listening port inside its network namespace. */
27
+ port: number;
28
+ /** Covered by the projection signature; it never enters the packet grant's identity. */
29
+ authorization: IRuntimeAuthorityReference;
30
+ }
31
+
32
+ /**
33
+ * Workload ingress selections plus retained denials. Only a projection of a `cloudly` controller may
34
+ * carry it; omitting it keeps the projection's canonical bytes and grants no workload ingress flow.
35
+ */
36
+ export interface IRuntimeNetworkWorkloadIngressProjection {
37
+ /**
38
+ * Strictly increasing by the canonical form of `[source.id, destination.id, protocol, port]`, so
39
+ * one entry per tuple. Each entry names at least one endpoint placed on this projection's node.
40
+ */
41
+ grants: IRuntimeNetworkWorkloadIngress[];
42
+ withdrawals: IRuntimeNetworkWorkloadIngressPacketGrant[];
43
+ }
44
+
45
+ export const runtimeNetworkWorkloadIngressContract = Object.freeze({
46
+ controllerKind: 'cloudly',
47
+ source: 'ingress-workload-lease',
48
+ destination: 'target-workload-lease-address-exact-port',
49
+ maximumGrants: 1024,
50
+ maximumWithdrawals: 4096,
51
+ } as const);
52
+
53
+ const grantKey = (value: {
54
+ source: IRuntimeAuthorityReference; destination: IRuntimeAuthorityReference; protocol: string; port: number;
55
+ }): string => networkCanonical([value.source.id, value.destination.id, value.protocol, value.port]);
56
+ const protocol = (value: unknown): 'tcp' | 'udp' => {
57
+ requireNetwork(value === 'tcp' || value === 'udp'); return value;
58
+ };
59
+
60
+ const packetGrant = (value: unknown): IRuntimeNetworkWorkloadIngressPacketGrant => {
61
+ const item = networkRecord(value, ['source', 'destination']);
62
+ const destination = networkRecord(item.destination, ['lease', 'protocol', 'port']);
63
+ return { source: networkReference(item.source), destination: { lease: networkReference(destination.lease),
64
+ protocol: protocol(destination.protocol), port: networkCounter(destination.port, 65535) } };
65
+ };
66
+
67
+ const grant = (value: unknown): IRuntimeNetworkWorkloadIngress => {
68
+ const item = networkRecord(value, ['source', 'destination', 'protocol', 'port', 'authorization']);
69
+ const result = { source: networkReference(item.source), destination: networkReference(item.destination),
70
+ protocol: protocol(item.protocol), port: networkCounter(item.port, 65535), authorization: networkReference(item.authorization) };
71
+ requireNetwork(result.source.id !== result.destination.id);
72
+ return result;
73
+ };
74
+
75
+ /** Internal parser: the enclosing projection has already captured caller input. */
76
+ export const snapshotWorkloadIngressProjection = (value: unknown): IRuntimeNetworkWorkloadIngressProjection => {
77
+ const item = networkRecord(value, ['grants', 'withdrawals']);
78
+ return {
79
+ grants: networkSorted(networkList(item.grants, runtimeNetworkWorkloadIngressContract.maximumGrants, grant), grantKey),
80
+ withdrawals: networkSorted(networkList(item.withdrawals, runtimeNetworkWorkloadIngressContract.maximumWithdrawals,
81
+ packetGrant), networkCanonical),
82
+ };
83
+ };
84
+
85
+ /** Internal composition after capture. The public helper validates the whole projection. */
86
+ export const workloadIngressPacketGrantsOf = (projection: {
87
+ controller: IRuntimeController;
88
+ nodeId: string;
89
+ runtimeNamespace: string;
90
+ workloadIngress?: IRuntimeNetworkWorkloadIngressProjection;
91
+ endpoints: Array<{ lease: IRuntimeNetworkWorkloadLease }>;
92
+ }): IRuntimeNetworkWorkloadIngressPacketGrant[] => {
93
+ const selected = projection.workloadIngress;
94
+ if (!selected) return [];
95
+ requireNetwork(projection.controller.kind === runtimeNetworkWorkloadIngressContract.controllerKind);
96
+ const leaseOf = (reference: IRuntimeAuthorityReference): IRuntimeNetworkWorkloadLease => {
97
+ const endpoint = projection.endpoints.find(candidate => networkEqual(networkReferenceOf(candidate.lease), reference));
98
+ requireNetwork(endpoint); return endpoint.lease;
99
+ };
100
+ const result = selected.grants.map((entry): IRuntimeNetworkWorkloadIngressPacketGrant => {
101
+ const source = leaseOf(entry.source), destination = leaseOf(entry.destination);
102
+ requireNetwork(networkSameScope(source, projection) || networkSameScope(destination, projection));
103
+ return { source: networkReferenceOf(source),
104
+ destination: { lease: networkReferenceOf(destination), protocol: entry.protocol, port: entry.port } };
105
+ });
106
+ return result.sort((left, right) => networkCanonical(left) < networkCanonical(right) ? -1 : 1);
107
+ };
@@ -0,0 +1,275 @@
1
+ import type { TSha256Digest } from './immutableimage.js';
2
+ import type { IRuntimeAssignment, IRuntimeAuthorityReference, IRuntimeController } from './runtimeassignment.js';
3
+ import { snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
4
+ import type { IRuntimeSessionBinding } from './runtimesession.js';
5
+ import { snapshotRuntimeSessionBinding } from './runtimesession.js';
6
+ import { captureNetwork, networkController, networkDigest, networkEqual, networkHash, networkId,
7
+ networkNodeId, networkRecord, networkReference, networkReferenceOf, networkRevision, requireNetwork,
8
+ } from '../private/networkcontract.js';
9
+ import { isRuntimeIdentityNumber } from '../private/runtimepolicy.js';
10
+ import { createStorageContractReaders } from '../private/storagecontract.js';
11
+
12
+ /**
13
+ * Where a local storage claim's bytes live, named by kind and never by path.
14
+ *
15
+ * `directory` is a node-local directory the node derives from the claim's identity under a root
16
+ * only the node knows. Network filesystems join this union as further kinds; their credentials
17
+ * travel as secrets and never inside a claim.
18
+ */
19
+ export type TRuntimeLocalStorageSource = { kind: 'directory' };
20
+
21
+ /** The owner and permission bits a node gives an `empty` volume's root directory when it creates it. */
22
+ export interface IRuntimeLocalStorageOwnership {
23
+ uid: number;
24
+ gid: number;
25
+ /** Numeric directory permission bits, for example `0o750`; setuid is refused. */
26
+ mode: number;
27
+ }
28
+
29
+ /** Where the volume appears inside the workload's container. */
30
+ export interface IRuntimeLocalStorageMount {
31
+ mountPath: string;
32
+ readOnly: boolean;
33
+ }
34
+
35
+ /**
36
+ * `bound` keeps the volume and lets the node mount it; `purge` orders the node to delete the
37
+ * volume's bytes. A purge always follows a bound generation of the same claim.
38
+ */
39
+ export type TRuntimeLocalStorageLifecycle = 'bound' | 'purge';
40
+
41
+ /**
42
+ * A controller's authority for one retained, node-local volume of one service.
43
+ *
44
+ * Cloudly and Onebox issue it alike. It names the volume by identity only: no host path, no
45
+ * device, no mount option and no credential. The node resolves the identity to its own storage.
46
+ *
47
+ * `initialization` says what the node does when the volume does not exist yet. `empty` creates an
48
+ * empty root with `initialOwnership`, which it must state, so a workload that runs as the image's
49
+ * own user never meets a root-owned directory by accident. `import` waits for the volume's bytes to
50
+ * be imported and states no ownership: an imported tree keeps the ownership it carries.
51
+ */
52
+ export interface IRuntimeLocalStorageClaim {
53
+ id: string;
54
+ generation: number;
55
+ previous: IRuntimeAuthorityReference | null;
56
+ controller: IRuntimeController;
57
+ organizationId: string;
58
+ clusterId: string;
59
+ nodeId: string;
60
+ runtimeNamespace: string;
61
+ serviceId: string;
62
+ /** The service's own name for this volume, stable across every generation of the claim. */
63
+ volumeId: string;
64
+ source: TRuntimeLocalStorageSource;
65
+ initialization: 'empty' | 'import';
66
+ /** Required for `empty`, `null` for `import`. */
67
+ initialOwnership: IRuntimeLocalStorageOwnership | null;
68
+ mount: IRuntimeLocalStorageMount;
69
+ accessMode: 'ReadWriteOnce';
70
+ reclaimPolicy: 'retain';
71
+ lifecycle: TRuntimeLocalStorageLifecycle;
72
+ digest: TSha256Digest;
73
+ }
74
+
75
+ /**
76
+ * What the node holds for the volume after admission.
77
+ *
78
+ * `ready` exists and may be mounted; `import-required` is an `import` volume whose bytes have not
79
+ * arrived; `purged` is a volume whose bytes the node deleted; `uncertain` is a volume the node
80
+ * cannot vouch for, which it neither mounts nor deletes until an operator decides.
81
+ */
82
+ export type TRuntimeLocalStorageClaimState = 'ready' | 'import-required' | 'purged' | 'uncertain';
83
+
84
+ /** Controller push over the node's current authenticated session. */
85
+ export interface IApplyRuntimeLocalStorageClaimRequest {
86
+ session: IRuntimeSessionBinding;
87
+ claim: IRuntimeLocalStorageClaim;
88
+ }
89
+
90
+ /**
91
+ * Durable admission acknowledgement. `claim` is the reference of the claim that was sent, also on
92
+ * a `historical` answer, whose `state` describes the newer generation the node holds.
93
+ */
94
+ export interface IApplyRuntimeLocalStorageClaimResponse {
95
+ status: 'accepted' | 'replay' | 'historical';
96
+ state: TRuntimeLocalStorageClaimState;
97
+ claim: IRuntimeAuthorityReference;
98
+ }
99
+
100
+ export const runtimeLocalStorageClaimContract = Object.freeze({
101
+ digestDomain: 'serve.zone/runtime-local-storage-claim',
102
+ error: 'Runtime local storage claim contract rejected.',
103
+ accessMode: 'ReadWriteOnce',
104
+ reclaimPolicy: 'retain',
105
+ /** Every permission bit a claim may state: setgid, sticky and the rwx triplets. */
106
+ directoryModeBits: 0o3777,
107
+ } as const);
108
+
109
+ const fail = (): never => { throw new Error(runtimeLocalStorageClaimContract.error); };
110
+ const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
111
+ const { readMountPath } = createStorageContractReaders(fail);
112
+ const claimStates = ['ready', 'import-required', 'purged', 'uncertain'] as const;
113
+ const responseStatuses = ['accepted', 'replay', 'historical'] as const;
114
+
115
+ const sourceOf = (valueArg: unknown): TRuntimeLocalStorageSource => {
116
+ const value = networkRecord(valueArg, ['kind']);
117
+ requireNetwork(value.kind === 'directory');
118
+ return { kind: 'directory' };
119
+ };
120
+
121
+ const ownershipOf = (valueArg: unknown): IRuntimeLocalStorageOwnership => {
122
+ const value = networkRecord(valueArg, ['uid', 'gid', 'mode']);
123
+ requireNetwork(isRuntimeIdentityNumber(value.uid) && isRuntimeIdentityNumber(value.gid)
124
+ && Number.isSafeInteger(value.mode) && (value.mode as number) >= 0
125
+ && ((value.mode as number) & ~runtimeLocalStorageClaimContract.directoryModeBits) === 0);
126
+ return { uid: value.uid as number, gid: value.gid as number, mode: value.mode as number };
127
+ };
128
+
129
+ const mountOf = (valueArg: unknown): IRuntimeLocalStorageMount => {
130
+ const value = networkRecord(valueArg, ['mountPath', 'readOnly']);
131
+ requireNetwork(typeof value.readOnly === 'boolean');
132
+ return { mountPath: readMountPath(value.mountPath, 'mountPath'), readOnly: value.readOnly };
133
+ };
134
+
135
+ /** Detached exact-schema snapshot. The digest is a separate check. */
136
+ export const snapshotRuntimeLocalStorageClaim = (valueArg: unknown): IRuntimeLocalStorageClaim => safe(() => {
137
+ const value = networkRecord(captureNetwork(valueArg), ['id', 'generation', 'previous', 'controller',
138
+ 'organizationId', 'clusterId', 'nodeId', 'runtimeNamespace', 'serviceId', 'volumeId', 'source',
139
+ 'initialization', 'initialOwnership', 'mount', 'accessMode', 'reclaimPolicy', 'lifecycle', 'digest']);
140
+ const { id, generation, previous } = networkRevision(value);
141
+ requireNetwork((value.initialization === 'empty' && value.initialOwnership !== null)
142
+ || (value.initialization === 'import' && value.initialOwnership === null));
143
+ requireNetwork(value.accessMode === 'ReadWriteOnce' && value.reclaimPolicy === 'retain');
144
+ requireNetwork(value.lifecycle === 'bound' || (value.lifecycle === 'purge' && previous !== null));
145
+ return {
146
+ id, generation, previous,
147
+ controller: networkController(value.controller),
148
+ organizationId: networkId(value.organizationId),
149
+ clusterId: networkId(value.clusterId),
150
+ nodeId: networkNodeId(value.nodeId),
151
+ runtimeNamespace: networkId(value.runtimeNamespace),
152
+ serviceId: networkId(value.serviceId),
153
+ volumeId: networkId(value.volumeId),
154
+ source: sourceOf(value.source),
155
+ initialization: value.initialization as IRuntimeLocalStorageClaim['initialization'],
156
+ initialOwnership: value.initialOwnership === null ? null : ownershipOf(value.initialOwnership),
157
+ mount: mountOf(value.mount),
158
+ accessMode: 'ReadWriteOnce',
159
+ reclaimPolicy: 'retain',
160
+ lifecycle: value.lifecycle as TRuntimeLocalStorageLifecycle,
161
+ digest: networkDigest(value.digest),
162
+ };
163
+ });
164
+
165
+ export const computeRuntimeLocalStorageClaimDigest = async (valueArg: unknown): Promise<TSha256Digest> => {
166
+ const claim = snapshotRuntimeLocalStorageClaim(valueArg);
167
+ try {
168
+ return await networkHash(runtimeLocalStorageClaimContract.digestDomain, claim);
169
+ } catch {
170
+ return fail();
171
+ }
172
+ };
173
+
174
+ export const validateRuntimeLocalStorageClaim = async (valueArg: unknown): Promise<string[]> => {
175
+ try {
176
+ const claim = snapshotRuntimeLocalStorageClaim(valueArg);
177
+ return claim.digest === await computeRuntimeLocalStorageClaimDigest(claim)
178
+ ? [] : ['runtime local storage claim digest does not match'];
179
+ } catch {
180
+ return ['runtime local storage claim must use its exact canonical schema'];
181
+ }
182
+ };
183
+
184
+ /** The `{ id, generation, digest }` an assignment's `workload.storage` seals for this claim. */
185
+ export const getRuntimeLocalStorageClaimReference = (valueArg: unknown): IRuntimeAuthorityReference =>
186
+ networkReferenceOf(snapshotRuntimeLocalStorageClaim(valueArg));
187
+
188
+ /**
189
+ * Content binding only: the assignment seals this exact claim, and both name the same controller,
190
+ * organization, node, runtime namespace and service. A purge claim binds to no assignment, because
191
+ * no workload may mount a volume being deleted. The cluster ID must come from the authenticated
192
+ * session, because an assignment does not contain one.
193
+ */
194
+ export const bindRuntimeLocalStorageClaimToAssignment = async (
195
+ claimArg: unknown, assignmentArg: unknown, expectedClusterIdArg: unknown,
196
+ ): Promise<{ claim: IRuntimeLocalStorageClaim; assignment: IRuntimeAssignment }> => {
197
+ const claim = snapshotRuntimeLocalStorageClaim(claimArg);
198
+ const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
199
+ if ((await validateRuntimeLocalStorageClaim(claim)).length
200
+ || (await validateRuntimeAssignment(assignment)).length) fail();
201
+ safe(() => {
202
+ const expectedClusterId = networkId(expectedClusterIdArg);
203
+ const reference = assignment.workload.storage.find((itemArg) => itemArg.id === claim.id);
204
+ requireNetwork(reference !== undefined
205
+ && networkEqual(reference, getRuntimeLocalStorageClaimReference(claim))
206
+ && claim.lifecycle === 'bound'
207
+ && claim.clusterId === expectedClusterId
208
+ && networkEqual(assignment.controller, claim.controller)
209
+ && assignment.organizationId === claim.organizationId
210
+ && assignment.nodeId === claim.nodeId
211
+ && assignment.runtimeNamespace === claim.runtimeNamespace
212
+ && assignment.serviceId === claim.serviceId);
213
+ });
214
+ return { claim, assignment };
215
+ };
216
+
217
+ export const snapshotApplyRuntimeLocalStorageClaimRequest = (
218
+ valueArg: unknown,
219
+ ): IApplyRuntimeLocalStorageClaimRequest => safe(() => {
220
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'claim']);
221
+ return { session: snapshotRuntimeSessionBinding(value.session), claim: snapshotRuntimeLocalStorageClaim(value.claim) };
222
+ });
223
+
224
+ export const snapshotApplyRuntimeLocalStorageClaimResponse = (
225
+ valueArg: unknown,
226
+ ): IApplyRuntimeLocalStorageClaimResponse => safe(() => {
227
+ const value = networkRecord(captureNetwork(valueArg), ['status', 'state', 'claim']);
228
+ requireNetwork((responseStatuses as readonly unknown[]).includes(value.status)
229
+ && (claimStates as readonly unknown[]).includes(value.state));
230
+ return {
231
+ status: value.status as IApplyRuntimeLocalStorageClaimResponse['status'],
232
+ state: value.state as TRuntimeLocalStorageClaimState,
233
+ claim: networkReference(value.claim),
234
+ };
235
+ });
236
+
237
+ /**
238
+ * Admission-side content binding, not authentication: `trustedSessionArg` must be the node's
239
+ * CURRENT authenticated session, never read from the request. The request's session must equal it,
240
+ * the claim must name its controller, cluster, node and runtime namespace, and its digest must hold.
241
+ */
242
+ export const bindApplyRuntimeLocalStorageClaimRequest = async (
243
+ valueArg: unknown, trustedSessionArg: unknown,
244
+ ): Promise<IApplyRuntimeLocalStorageClaimRequest> => {
245
+ const request = snapshotApplyRuntimeLocalStorageClaimRequest(valueArg);
246
+ const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
247
+ if ((await validateRuntimeLocalStorageClaim(request.claim)).length) fail();
248
+ safe(() => requireNetwork(networkEqual(request.session, trustedSession)
249
+ && networkEqual(request.claim.controller, trustedSession.controller)
250
+ && request.claim.clusterId === trustedSession.clusterId
251
+ && request.claim.nodeId === trustedSession.nodeId
252
+ && request.claim.runtimeNamespace === trustedSession.runtimeNamespace));
253
+ return request;
254
+ };
255
+
256
+ /**
257
+ * Matches an answer to the exact claim that was sent. An `accepted` or `replay` answer must state a
258
+ * state the claim can be in: a bound claim is `ready`, `import-required` (for an `import` claim
259
+ * only) or `uncertain`; a purge is `purged` or `uncertain`. A `historical` answer describes the
260
+ * newer generation the node holds, so its state is not judged against the sent claim.
261
+ */
262
+ export const bindApplyRuntimeLocalStorageClaimResponse = (
263
+ valueArg: unknown, requestArg: unknown,
264
+ ): IApplyRuntimeLocalStorageClaimResponse => safe(() => {
265
+ const response = snapshotApplyRuntimeLocalStorageClaimResponse(valueArg);
266
+ const { claim } = snapshotApplyRuntimeLocalStorageClaimRequest(requestArg);
267
+ requireNetwork(networkEqual(response.claim, getRuntimeLocalStorageClaimReference(claim)));
268
+ if (response.status !== 'historical') {
269
+ requireNetwork(claim.lifecycle === 'purge'
270
+ ? response.state === 'purged' || response.state === 'uncertain'
271
+ : response.state === 'ready' || response.state === 'uncertain'
272
+ || (response.state === 'import-required' && claim.initialization === 'import'));
273
+ }
274
+ return response;
275
+ });
@@ -17,6 +17,7 @@ import type {
17
17
  IServiceSecretConfiguration,
18
18
  IServiceSecretDeploymentPlan,
19
19
  } from './secret.js';
20
+ import type { TServiceVolumeInitialization } from './servicevolumeinitialization.js';
20
21
 
21
22
  export interface IServiceVolume {
22
23
  /** Stable Docker volume name. If omitted, Coreflow derives one from service id and mount path. */
@@ -32,8 +33,17 @@ export interface IServiceVolume {
32
33
  backup?: boolean;
33
34
  /** Driver-specific options forwarded to Docker's VolumeDriver.Create request. */
34
35
  options?: Record<string, string>;
36
+ /**
37
+ * Server-managed: how the volume's node-local storage claim starts on a Pallet cluster. Only
38
+ * `setServiceVolumeInitialization` writes it; a volume without one is refused as
39
+ * `volume-initialization-unknown` instead of being created empty.
40
+ */
41
+ initialization?: TServiceVolumeInitialization;
35
42
  }
36
43
 
44
+ /** Caller-writable volume declaration. Its initialization has its own fenced request. */
45
+ export type TServiceWritableVolume = Omit<IServiceVolume, 'initialization'>;
46
+
37
47
  const organizationIdRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
38
48
 
39
49
  /** Validates the canonical organization authority used by service operations. */
@@ -255,12 +265,18 @@ export type TServiceWritableData = Omit<
255
265
  | 'secretConfiguration'
256
266
  | 'secretDeployment'
257
267
  | 'mail'
268
+ | 'volumes'
258
269
  > & {
259
270
  /**
260
271
  * Mail configuration stays caller-writable, except for the CoreMail binding
261
272
  * metadata Cloudly derives from reconciliation.
262
273
  */
263
274
  mail?: TServiceMailWritableConfig;
275
+ /**
276
+ * Volumes stay caller-writable, except for their initialization, which Cloudly refuses here.
277
+ * A stored initialization stays with the volume of the same mount path and goes with it.
278
+ */
279
+ volumes?: TServiceWritableVolume[];
264
280
  };
265
281
 
266
282
  /** Caller-writable update data. Null explicitly removes an existing argument vector. */
@@ -76,19 +76,28 @@ export const serviceRuntimeContract = Object.freeze({
76
76
  maximumAssignmentPageSize: 256,
77
77
  } as const);
78
78
 
79
- const serviceRuntimeWaitingReasons = ['node-network-pending', 'conflict', 'failure-backoff'] as const;
79
+ const serviceRuntimeWaitingReasons = [
80
+ 'node-network-pending', 'conflict', 'failure-backoff', 'storage-import-pending', 'bindings-pending',
81
+ 'legacy-adoption-pending',
82
+ ] as const;
80
83
  const serviceRuntimeRefusals = [
81
84
  'cluster-runtime-missing', 'organization-missing', 'image-plan-missing', 'platform-unsupported',
82
85
  'secrets-unsupported', 'volumes-unsupported', 'port-protocol-unsupported', 'ports-too-many',
83
86
  'readiness-target-undeclared', 'environment-too-large', 'no-eligible-node', 'replica-limit',
84
87
  'node-authority-unsupported', 'network-policy-missing', 'network-policy-mismatch',
85
88
  'network-workload-pool-missing', 'network-workload-pool-exhausted',
89
+ 'volume-driver-unsupported', 'volume-options-unsupported', 'volume-shared-unsupported',
90
+ 'volume-placement-unsupported', 'volume-initialization-unknown', 'volume-ownership-unknown',
86
91
  ] as const;
87
92
 
88
93
  /**
89
94
  * Why the producer is not converging yet, though nothing needs an administrator:
90
95
  * `node-network-pending` waits for a node's network readiness, `conflict` for a sweep over inputs
91
- * that changed while it ran, `failure-backoff` for a failed replica's backoff to elapse.
96
+ * that changed while it ran, `failure-backoff` for a failed replica's backoff to elapse,
97
+ * `storage-import-pending` for an `import` local storage claim whose bytes have not arrived on its
98
+ * node, `bindings-pending` for the secret manifest to cover every enabled binding of the service, and
99
+ * `legacy-adoption-pending` for a Corestore binding whose legacy resource waits for the offline
100
+ * adoption Corestore requires (the binding's `statusReason` of the same name).
92
101
  */
93
102
  export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[number];
94
103
 
@@ -101,6 +110,14 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
101
110
  * node whose session resolved fewer workload authorities than this spec needs, which the admit step
102
111
  * refuses rather than handing the node work it would silently park. An attached network needs an
103
112
  * explicit matching policy and a current workload address pool; neither is defaulted.
113
+ *
114
+ * The volume refusals name what a local storage claim cannot express: `volume-driver-unsupported`
115
+ * a volume of a driver other than a node-local directory, `volume-options-unsupported` driver or
116
+ * mount options, `volume-shared-unsupported` one volume mounted by more than one replica or service,
117
+ * and `volume-placement-unsupported` a placement that does not pin each volume to one node.
118
+ * `volume-initialization-unknown` is a volume without an initialization and
119
+ * `volume-ownership-unknown` a `run-as` ownership under `runAs: null`
120
+ * (`deriveServiceVolumeClaimInitialization`); neither is defaulted.
104
121
  */
105
122
  export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
106
123