@serve.zone/interfaces 32.18.0 → 32.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +59 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/deploymentpreflight.d.ts +7 -1
- package/dist_ts/data/deploymentpreflight.js +59 -1
- package/dist_ts/data/immutableimage.d.ts +6 -0
- package/dist_ts/data/immutableimage.js +7 -1
- package/dist_ts/data/index.d.ts +2 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/networkhost.d.ts +61 -0
- package/dist_ts/data/networkhost.js +52 -0
- package/dist_ts/data/networkprojection.d.ts +9 -0
- package/dist_ts/data/networkprojection.js +21 -3
- package/dist_ts/data/noderuntimebinding.d.ts +79 -1
- package/dist_ts/data/noderuntimebinding.js +111 -3
- package/dist_ts/data/runtimesession.d.ts +22 -1
- package/dist_ts/data/runtimesession.js +43 -2
- package/dist_ts/requests/runtimesession.d.ts +13 -0
- package/package.json +6 -6
- package/readme.md +71 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/deploymentpreflight.ts +60 -46
- package/ts/data/immutableimage.ts +8 -0
- package/ts/data/index.ts +2 -0
- package/ts/data/networkhost.ts +98 -0
- package/ts/data/networkprojection.ts +27 -2
- package/ts/data/noderuntimebinding.ts +163 -3
- package/ts/data/runtimesession.ts +50 -2
- package/ts/requests/runtimesession.ts +17 -0
|
@@ -7,6 +7,8 @@ import { bindRuntimeNetworkHandoffLease, computeRuntimeNetworkProtectedAuthority
|
|
|
7
7
|
import type { IPrivateNetwork, IServicePrivateNetworks, IServicePrivateNetworkDnsPolicy } from './privatenetwork.js';
|
|
8
8
|
import { routerPacketGrantsOf, snapshotRouterProjection,
|
|
9
9
|
type IRuntimeNetworkRouterPacketGrant, type IRuntimeNetworkRouterProjection } from './networkrouter.js';
|
|
10
|
+
import { hostPacketGrantsOf, snapshotHostProjection,
|
|
11
|
+
type IRuntimeNetworkHostPacketGrant, type IRuntimeNetworkHostProjection } from './networkhost.js';
|
|
10
12
|
import { buildPrivateNetworkFqdn, composeServicePrivateNetworkDnsPolicy,
|
|
11
13
|
snapshotPrivateNetwork, snapshotServicePrivateNetworks } from './privatenetwork.js';
|
|
12
14
|
import { captureNetwork, failNetwork, networkCanonical, networkContains, networkCounter, networkDigest,
|
|
@@ -159,6 +161,12 @@ export interface IRuntimeNetworkProjection extends IRuntimeNetworkNodeScope {
|
|
|
159
161
|
* projection's canonical bytes and grants no router-origin flows.
|
|
160
162
|
*/
|
|
161
163
|
router?: IRuntimeNetworkRouterProjection;
|
|
164
|
+
/**
|
|
165
|
+
* Host-origin selections of a `onebox` projection: the node's own host dials exact local workload
|
|
166
|
+
* ports (`IRuntimeNetworkHostPacketGrant`). Omitted, it keeps the canonical bytes and grants no
|
|
167
|
+
* host-origin flow; any other controller kind carrying it is refused.
|
|
168
|
+
*/
|
|
169
|
+
host?: IRuntimeNetworkHostProjection;
|
|
162
170
|
digest: TSha256Digest;
|
|
163
171
|
}
|
|
164
172
|
|
|
@@ -313,9 +321,10 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
313
321
|
const captured = captureNetwork(value);
|
|
314
322
|
requireNetwork(new TextEncoder().encode(networkCanonical(captured)).length <= runtimeNetworkProjectionContract.maximumCanonicalBytes);
|
|
315
323
|
const hasRouter = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'router');
|
|
324
|
+
const hasHost = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'host');
|
|
316
325
|
const item = networkRecord(captured, ['id', 'generation', 'previous', 'controller', 'nodeId', 'runtimeNamespace',
|
|
317
326
|
'protectedAuthority', 'allocationAuthorities', 'handoff', 'networks', 'memberships', 'endpoints', 'dns', 'withdrawals', 'retiredLeases', 'retiredHandoffs', 'digest',
|
|
318
|
-
...(hasRouter ? ['router'] : [])]);
|
|
327
|
+
...(hasRouter ? ['router'] : []), ...(hasHost ? ['host'] : [])]);
|
|
319
328
|
return { ...networkRevision(item), ...networkScope(item),
|
|
320
329
|
protectedAuthority: snapshotRuntimeNetworkProtectedAuthority(item.protectedAuthority),
|
|
321
330
|
allocationAuthorities: networkSorted(networkList(item.allocationAuthorities, 16, snapshotRuntimeNetworkProtectedAuthority), refKey),
|
|
@@ -325,6 +334,7 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
325
334
|
endpoints: networkSorted(networkList(item.endpoints, 256, endpoint), value => value.lease.id), dns: networkDnsWindow(item.dns),
|
|
326
335
|
withdrawals: networkSorted(networkList(item.withdrawals, 4096, packetGrant), networkCanonical),
|
|
327
336
|
...(hasRouter ? { router: snapshotRouterProjection(item.router) } : {}),
|
|
337
|
+
...(hasHost ? { host: snapshotHostProjection(item.host) } : {}),
|
|
328
338
|
retiredLeases: sortedReferences(item.retiredLeases), retiredHandoffs: sortedReferences(item.retiredHandoffs), digest: networkDigest(item.digest) };
|
|
329
339
|
});
|
|
330
340
|
|
|
@@ -402,6 +412,8 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
402
412
|
const assignments = new Set<string>(), attempts = new Set<string>(), readyReplicas = new Set<string>(), hostPorts = new Set<string>();
|
|
403
413
|
for (const endpoint of projection.endpoints) {
|
|
404
414
|
const lease = endpoint.lease, authority = allocationAuthority(lease.protectedAuthority);
|
|
415
|
+
// A controller on the node itself has no other node: every endpoint it projects is local.
|
|
416
|
+
requireNetwork(projection.controller.kind !== 'onebox' || networkSameScope(lease, projection));
|
|
405
417
|
requireNetwork(networkEqual(lease.controller, projection.controller) && members.has(ownerKey(lease))
|
|
406
418
|
&& !assignments.has(lease.assignmentId) && !attempts.has(attemptKey(lease)));
|
|
407
419
|
assignments.add(lease.assignmentId); attempts.add(attemptKey(lease));
|
|
@@ -430,6 +442,8 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
430
442
|
requireNetwork(projection.withdrawals.every(grant => !grants.has(networkCanonical(grant))));
|
|
431
443
|
const routerGrants = new Set(routerPacketGrantsOf(projection).map(networkCanonical));
|
|
432
444
|
requireNetwork((projection.router?.withdrawals ?? []).every(grant => !routerGrants.has(networkCanonical(grant))));
|
|
445
|
+
const hostGrants = new Set(hostPacketGrantsOf(projection).map(networkCanonical));
|
|
446
|
+
requireNetwork((projection.host?.withdrawals ?? []).every(grant => !hostGrants.has(networkCanonical(grant))));
|
|
433
447
|
requireNetwork(projection.digest === await computeRuntimeNetworkProjectionDigest(projection));
|
|
434
448
|
return projection;
|
|
435
449
|
} catch { return failNetwork(); }
|
|
@@ -443,6 +457,10 @@ export const getRuntimeNetworkProjectionPacketGrants = async (value: unknown): P
|
|
|
443
457
|
export const getRuntimeNetworkProjectionRouterPacketGrants = async (value: unknown): Promise<IRuntimeNetworkRouterPacketGrant[]> =>
|
|
444
458
|
routerPacketGrantsOf(await validateRuntimeNetworkProjection(value));
|
|
445
459
|
|
|
460
|
+
/** Host-origin authority comes only from explicit signed `onebox` selections of local workload ports. */
|
|
461
|
+
export const getRuntimeNetworkProjectionHostPacketGrants = async (value: unknown): Promise<IRuntimeNetworkHostPacketGrant[]> =>
|
|
462
|
+
hostPacketGrantsOf(await validateRuntimeNetworkProjection(value));
|
|
463
|
+
|
|
446
464
|
/** Complete offline DNS material, including authorized names never previously queried. */
|
|
447
465
|
/** Select exact local receivers before expansion; all targets still come from
|
|
448
466
|
* the complete validated projection. Selection supplies no attachment authority. */
|
|
@@ -507,7 +525,7 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
507
525
|
requireNetwork(networkSameScope(projection, trusted));
|
|
508
526
|
if (previousInput === null) {
|
|
509
527
|
requireNetwork(projection.generation === 1 && applied === null && projection.withdrawals.length === 0
|
|
510
|
-
&& (projection.router?.withdrawals.length ?? 0) === 0
|
|
528
|
+
&& (projection.router?.withdrawals.length ?? 0) === 0 && (projection.host?.withdrawals.length ?? 0) === 0
|
|
511
529
|
&& projection.retiredLeases.length === 0 && projection.retiredHandoffs.length === 0);
|
|
512
530
|
return { status: 'accepted', projection };
|
|
513
531
|
}
|
|
@@ -543,6 +561,12 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
543
561
|
for (const grant of previousRouterGrants) requireNetwork(routerGrants.has(networkCanonical(grant)) || routerWithdrawals.has(networkCanonical(grant)));
|
|
544
562
|
const knownRouterWithdrawals = new Set([...previousRouterGrants, ...(previous.router?.withdrawals ?? [])].map(networkCanonical));
|
|
545
563
|
requireNetwork((projection.router?.withdrawals ?? []).every(grant => knownRouterWithdrawals.has(networkCanonical(grant))));
|
|
564
|
+
const hostGrants = new Set(hostPacketGrantsOf(projection).map(networkCanonical));
|
|
565
|
+
const hostWithdrawals = new Set((projection.host?.withdrawals ?? []).map(networkCanonical));
|
|
566
|
+
const previousHostGrants = hostPacketGrantsOf(previous);
|
|
567
|
+
for (const grant of previousHostGrants) requireNetwork(hostGrants.has(networkCanonical(grant)) || hostWithdrawals.has(networkCanonical(grant)));
|
|
568
|
+
const knownHostWithdrawals = new Set([...previousHostGrants, ...(previous.host?.withdrawals ?? [])].map(networkCanonical));
|
|
569
|
+
requireNetwork((projection.host?.withdrawals ?? []).every(grant => knownHostWithdrawals.has(networkCanonical(grant))));
|
|
546
570
|
const knownLeases = [...previous.endpoints.map(endpoint => networkReferenceOf(endpoint.lease)), ...previous.retiredLeases];
|
|
547
571
|
const knownHandoffs = [...(previous.handoff ? [networkReferenceOf(previous.handoff)] : []), ...previous.retiredHandoffs];
|
|
548
572
|
requireNetwork(projection.retiredLeases.every(retired => knownLeases.some(known => networkEqual(known, retired)))
|
|
@@ -553,6 +577,7 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
553
577
|
if (applied === null) {
|
|
554
578
|
for (const grant of previous.withdrawals) requireNetwork(withdrawn.has(networkCanonical(grant)));
|
|
555
579
|
for (const grant of previous.router?.withdrawals ?? []) requireNetwork(routerWithdrawals.has(networkCanonical(grant)));
|
|
580
|
+
for (const grant of previous.host?.withdrawals ?? []) requireNetwork(hostWithdrawals.has(networkCanonical(grant)));
|
|
556
581
|
requireCarried(previous.retiredLeases, projection.retiredLeases); requireCarried(previous.retiredHandoffs, projection.retiredHandoffs);
|
|
557
582
|
}
|
|
558
583
|
for (const old of previous.endpoints) {
|
|
@@ -4,9 +4,10 @@ import type { IClusterRuntimeReference } from './clusterruntime.js';
|
|
|
4
4
|
import { snapshotClusterRuntimeReference } from './clusterruntime.js';
|
|
5
5
|
import type { IPalletNodeEnrollmentActive } from './nodeenrollment.js';
|
|
6
6
|
import { snapshotPalletNodeEnrollmentConfirmation } from './nodeenrollment.js';
|
|
7
|
-
import type { IRuntimeController } from './runtimeassignment.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import type { IRuntimeAssignment, IRuntimeController } from './runtimeassignment.js';
|
|
8
|
+
import { isCanonicalRegistryHost } from './immutableimage.js';
|
|
9
|
+
import { compareWireTokens, deepFreezeValue } from '../private/canonicaljson.js';
|
|
10
|
+
import { isPlainRecord, readDataElements, readDataProperty, readExactDataRecord } from '../private/dataproperties.js';
|
|
10
11
|
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
11
12
|
import { networkController, networkId } from '../private/networkcontract.js';
|
|
12
13
|
|
|
@@ -48,6 +49,70 @@ export const nodeRuntimeBindingContract = Object.freeze({
|
|
|
48
49
|
runtimeAuthority: 'fresh-authenticated-controller-session-only',
|
|
49
50
|
} as const);
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Durable routing identity for one Pallet node driven by a controller on the same host.
|
|
54
|
+
*
|
|
55
|
+
* The local counterpart of `INodeRuntimeRoutingBinding`. It names no Cloudly origin and no relay:
|
|
56
|
+
* the controller reaches Pallet over Pallet's root-only Unix socket
|
|
57
|
+
* (`nodeRuntimeLocalControllerContract.socketPath`), whose file permissions authenticate the peer.
|
|
58
|
+
* A node carries a Cloudly binding or a local one, never both. Like the Cloudly binding it grants
|
|
59
|
+
* nothing by itself; runtime authority still comes only from a fresh session on that socket.
|
|
60
|
+
*/
|
|
61
|
+
export interface INodeRuntimeLocalRoutingBinding {
|
|
62
|
+
nodeId: string;
|
|
63
|
+
clusterId: string;
|
|
64
|
+
controller: IRuntimeController & { kind: 'onebox' };
|
|
65
|
+
runtimeNamespace: string;
|
|
66
|
+
/**
|
|
67
|
+
* The registries whose pull credential this controller issues, as canonical `host[:port]`,
|
|
68
|
+
* strictly increasing. A local controller has no Cloudly origin to stand for its registry, so a
|
|
69
|
+
* node asks it for a credential only for an image whose `registryHost`, and `pullEndpoint` when
|
|
70
|
+
* one is stated, are both listed here (`isNodeRuntimeLocalRegistryWorkload`).
|
|
71
|
+
*/
|
|
72
|
+
registryHosts: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The credential a node registers its local session with, as the controller may pin it.
|
|
77
|
+
* Pallet generates the bearer and keeps it; the controller only ever learns its hash.
|
|
78
|
+
*/
|
|
79
|
+
export interface INodeRuntimeLocalCredentialAcknowledgement {
|
|
80
|
+
generation: number;
|
|
81
|
+
/** Lowercase hex SHA-256 of the bearer's UTF-8 bytes. Sensitive transport field: never log it. */
|
|
82
|
+
tokenHash: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Pallet's answer to a local bind: the exact persisted binding and the credential behind it. */
|
|
86
|
+
export interface INodeRuntimeLocalBindingConfirmation {
|
|
87
|
+
binding: INodeRuntimeLocalRoutingBinding;
|
|
88
|
+
credential: INodeRuntimeLocalCredentialAcknowledgement;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* How a controller on the node's own host reaches Pallet.
|
|
93
|
+
*
|
|
94
|
+
* The controller is the TypedSocket client and Pallet the server, but the request directions of the
|
|
95
|
+
* runtime session family are unchanged: Pallet still registers (`registerPalletRuntimeSession`, fired
|
|
96
|
+
* at the connected controller) and reports, and the controller still applies. The first request on
|
|
97
|
+
* a connection is `bindPalletLocalRuntimeController`: the first bind persists the binding and a
|
|
98
|
+
* freshly generated bearer, every later bind must be the exact same binding and answers the same
|
|
99
|
+
* credential. The controller pins that credential at its first bind and refuses a different one
|
|
100
|
+
* afterwards, because a node that answers another credential for the same binding has lost the
|
|
101
|
+
* state this controller enrolled.
|
|
102
|
+
*/
|
|
103
|
+
export const nodeRuntimeLocalControllerContract = Object.freeze({
|
|
104
|
+
socketPath: '/run/serve.zone/pallet/controller.sock',
|
|
105
|
+
socketMode: 0o600,
|
|
106
|
+
/** TypedSocket server URL over the socket: it supplies only the Host header and request path. */
|
|
107
|
+
serverUrl: 'http://localhost',
|
|
108
|
+
transport: 'controller-initiated-typedsocket-on-root-only-unix-socket',
|
|
109
|
+
localMutation: 'first-bind-or-exact-replay',
|
|
110
|
+
exclusivity: 'cloudly-or-local-controller-never-both',
|
|
111
|
+
credential: 'pallet-generated-bearer-controller-pins-hash-at-first-bind',
|
|
112
|
+
registryAuthority: 'listed-registry-hosts-only',
|
|
113
|
+
maximumRegistryHosts: 16,
|
|
114
|
+
} as const);
|
|
115
|
+
|
|
51
116
|
const fail = (): never => { throw new Error('Node runtime binding contract rejected.'); };
|
|
52
117
|
function requireValue(conditionArg: unknown): asserts conditionArg {
|
|
53
118
|
if (!conditionArg) fail();
|
|
@@ -78,6 +143,26 @@ const relay = (valueArg: unknown): IClusterRelay => {
|
|
|
78
143
|
return { origin: value.origin as string };
|
|
79
144
|
};
|
|
80
145
|
const cloudlyOrigin = (valueArg: unknown): string => relay({ origin: valueArg }).origin;
|
|
146
|
+
const registryHosts = (valueArg: unknown): string[] => {
|
|
147
|
+
const elements = readDataElements(valueArg, nodeRuntimeLocalControllerContract.maximumRegistryHosts);
|
|
148
|
+
requireValue(elements !== null && elements.length > 0);
|
|
149
|
+
elements.forEach((hostArg, indexArg) => requireValue(isCanonicalRegistryHost(hostArg)
|
|
150
|
+
&& (indexArg === 0 || compareWireTokens(elements[indexArg - 1] as string, hostArg) < 0)));
|
|
151
|
+
return elements as string[];
|
|
152
|
+
};
|
|
153
|
+
const localController = (valueArg: unknown): IRuntimeController & { kind: 'onebox' } => {
|
|
154
|
+
const controller = networkController(record(valueArg, ['kind', 'id', 'epoch']));
|
|
155
|
+
requireValue(controller.kind === 'onebox');
|
|
156
|
+
return { kind: 'onebox', id: controller.id, epoch: controller.epoch };
|
|
157
|
+
};
|
|
158
|
+
const tokenHash = (valueArg: unknown): string => {
|
|
159
|
+
requireValue(typeof valueArg === 'string' && /^[a-f0-9]{64}$/.test(valueArg));
|
|
160
|
+
return valueArg;
|
|
161
|
+
};
|
|
162
|
+
const localCredential = (valueArg: unknown): INodeRuntimeLocalCredentialAcknowledgement => {
|
|
163
|
+
const value = record(valueArg, ['generation', 'tokenHash']);
|
|
164
|
+
return { generation: positive(value.generation), tokenHash: tokenHash(value.tokenHash) };
|
|
165
|
+
};
|
|
81
166
|
const activeEnrollment = (valueArg: unknown): IPalletNodeEnrollmentActive => {
|
|
82
167
|
const value = snapshotPalletNodeEnrollmentConfirmation(valueArg);
|
|
83
168
|
requireValue(value.phase === 'active');
|
|
@@ -189,3 +274,78 @@ export const bindPalletNodeRuntimeBindingRead = (
|
|
|
189
274
|
&& requestedOrigin === active.cloudlyOrigin && requestedNodeId === active.nodeId);
|
|
190
275
|
return response;
|
|
191
276
|
};
|
|
277
|
+
|
|
278
|
+
/** Exact detached snapshot of a local routing binding; shape checks are not authentication. */
|
|
279
|
+
export const snapshotNodeRuntimeLocalRoutingBinding = (
|
|
280
|
+
valueArg: unknown,
|
|
281
|
+
): INodeRuntimeLocalRoutingBinding => {
|
|
282
|
+
try {
|
|
283
|
+
const value = record(valueArg,
|
|
284
|
+
['nodeId', 'clusterId', 'controller', 'runtimeNamespace', 'registryHosts']);
|
|
285
|
+
return deepFreezeValue({
|
|
286
|
+
nodeId: nodeId(value.nodeId), clusterId: networkId(value.clusterId),
|
|
287
|
+
controller: localController(value.controller), runtimeNamespace: networkId(value.runtimeNamespace),
|
|
288
|
+
registryHosts: registryHosts(value.registryHosts),
|
|
289
|
+
});
|
|
290
|
+
} catch {
|
|
291
|
+
return fail();
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/** Exact detached snapshot of Pallet's answer to a local bind. */
|
|
296
|
+
export const snapshotNodeRuntimeLocalBindingConfirmation = (
|
|
297
|
+
valueArg: unknown,
|
|
298
|
+
): INodeRuntimeLocalBindingConfirmation => {
|
|
299
|
+
try {
|
|
300
|
+
const value = record(valueArg, ['binding', 'credential']);
|
|
301
|
+
return deepFreezeValue({ binding: snapshotNodeRuntimeLocalRoutingBinding(value.binding),
|
|
302
|
+
credential: localCredential(value.credential) });
|
|
303
|
+
} catch {
|
|
304
|
+
return fail();
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Controller-side binding of Pallet's answer to the exact binding it sent. `pinnedArg` must come
|
|
310
|
+
* from the controller's own durable state: the credential it pinned at its first bind, or null for
|
|
311
|
+
* that first bind. A different credential for the same binding is refused, never adopted.
|
|
312
|
+
*/
|
|
313
|
+
export const bindNodeRuntimeLocalBindingConfirmation = (
|
|
314
|
+
responseArg: unknown,
|
|
315
|
+
requestArg: unknown,
|
|
316
|
+
pinnedArg: unknown,
|
|
317
|
+
): INodeRuntimeLocalBindingConfirmation => {
|
|
318
|
+
try {
|
|
319
|
+
const response = snapshotNodeRuntimeLocalBindingConfirmation(responseArg);
|
|
320
|
+
const request = snapshotNodeRuntimeLocalRoutingBinding(requestArg);
|
|
321
|
+
requireValue(JSON.stringify(response.binding) === JSON.stringify(request));
|
|
322
|
+
if (pinnedArg !== null) {
|
|
323
|
+
const pinned = localCredential(pinnedArg);
|
|
324
|
+
requireValue(pinned.generation === response.credential.generation
|
|
325
|
+
&& pinned.tokenHash === response.credential.tokenHash);
|
|
326
|
+
}
|
|
327
|
+
return response;
|
|
328
|
+
} catch {
|
|
329
|
+
return fail();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Whether a local controller may issue the pull credential for this workload's image: its
|
|
335
|
+
* `registryHost`, and its `pullEndpoint` when one is stated, are both listed in the binding.
|
|
336
|
+
* The workload is read as a wire value; anything this cannot read is not authorized.
|
|
337
|
+
*/
|
|
338
|
+
export const isNodeRuntimeLocalRegistryWorkload = (
|
|
339
|
+
bindingArg: unknown,
|
|
340
|
+
workloadArg: IRuntimeAssignment['workload'],
|
|
341
|
+
): boolean => {
|
|
342
|
+
try {
|
|
343
|
+
const binding = snapshotNodeRuntimeLocalRoutingBinding(bindingArg);
|
|
344
|
+
const listed = (propertyArg: { value: unknown } | null): boolean => propertyArg !== null
|
|
345
|
+
&& typeof propertyArg.value === 'string' && binding.registryHosts.includes(propertyArg.value);
|
|
346
|
+
return isPlainRecord(workloadArg) && listed(readDataProperty(workloadArg, 'registryHost'))
|
|
347
|
+
&& (!Object.hasOwn(workloadArg, 'pullEndpoint') || listed(readDataProperty(workloadArg, 'pullEndpoint')));
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import { negotiateProtocol, readProtocolOffer, type IProtocolOffer } from '../protocol.js';
|
|
12
12
|
import type { IRuntimeConfig } from './runtimeconfig.js';
|
|
13
13
|
import { bindRuntimeConfigToAssignment, snapshotRuntimeConfig } from './runtimeconfig.js';
|
|
14
|
-
import {
|
|
14
|
+
import { snapshotNodeRuntimeLocalBindingConfirmation, snapshotNodeRuntimeLocalRoutingBinding } from './noderuntimebinding.js';
|
|
15
|
+
import { canonicalizeStrictJson, createSha256Hex } from '../private/canonicaljson.js';
|
|
15
16
|
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -32,7 +33,11 @@ export interface IRuntimeSessionBinding {
|
|
|
32
33
|
runtimeNamespace: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Pallet-initiated WSS on its configured Cloudly HTTPS origin; never HTTP RPC. A node bound to a
|
|
38
|
+
* local controller fires the same request at the controller connected to its root-only Unix socket
|
|
39
|
+
* (`nodeRuntimeLocalControllerContract`), with the bearer whose hash its local bind answered.
|
|
40
|
+
*/
|
|
36
41
|
export interface IRegisterPalletRuntimeSessionRequest {
|
|
37
42
|
nodeId: string;
|
|
38
43
|
credentialGeneration: number;
|
|
@@ -395,6 +400,49 @@ export const bindRegisterPalletRuntimeSessionResponse = (
|
|
|
395
400
|
return response;
|
|
396
401
|
});
|
|
397
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Bind a local controller's registration reply to the node's own local routing binding.
|
|
405
|
+
*
|
|
406
|
+
* The counterpart of `bindRegisterPalletRuntimeSessionResponse` for a node bound to a controller on
|
|
407
|
+
* its own host: the answered session must name exactly the binding's controller, node, cluster and
|
|
408
|
+
* runtime namespace, the credential generation sent, and an offer compatible with the one sent.
|
|
409
|
+
* `bindingArg` must come from the node's own durable state, never from the reply.
|
|
410
|
+
*/
|
|
411
|
+
export const bindRegisterPalletLocalRuntimeSessionResponse = (
|
|
412
|
+
valueArg: unknown, requestArg: unknown, bindingArg: unknown,
|
|
413
|
+
): IRegisterPalletRuntimeSessionResponse => safe(() => {
|
|
414
|
+
const response = snapshotRegisterPalletRuntimeSessionResponse(valueArg);
|
|
415
|
+
const request = snapshotRegisterPalletRuntimeSessionRequest(requestArg);
|
|
416
|
+
const binding = snapshotNodeRuntimeLocalRoutingBinding(bindingArg);
|
|
417
|
+
requireValue(canonical(response.session.controller) === canonical(binding.controller)
|
|
418
|
+
&& request.nodeId === binding.nodeId && response.session.nodeId === binding.nodeId
|
|
419
|
+
&& response.session.clusterId === binding.clusterId
|
|
420
|
+
&& response.session.runtimeNamespace === binding.runtimeNamespace
|
|
421
|
+
&& response.session.credentialGeneration === request.credentialGeneration
|
|
422
|
+
&& negotiateProtocol(request.protocol, response.protocol).compatible);
|
|
423
|
+
return response;
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Controller-side check of a local node's registration against the credential this controller
|
|
428
|
+
* pinned at its first local bind. `confirmationArg` must come from the controller's own durable
|
|
429
|
+
* state, never from the request. This proves the bearer is the one that bind acknowledged; the
|
|
430
|
+
* socket's file permissions remain the peer authentication, and session admission stays the
|
|
431
|
+
* controller's.
|
|
432
|
+
*/
|
|
433
|
+
export const bindRegisterPalletLocalRuntimeSessionRequest = async (
|
|
434
|
+
valueArg: unknown, confirmationArg: unknown,
|
|
435
|
+
): Promise<IRegisterPalletRuntimeSessionRequest> => {
|
|
436
|
+
try {
|
|
437
|
+
const request = snapshotRegisterPalletRuntimeSessionRequest(valueArg);
|
|
438
|
+
const confirmation = snapshotNodeRuntimeLocalBindingConfirmation(confirmationArg);
|
|
439
|
+
requireValue(request.nodeId === confirmation.binding.nodeId
|
|
440
|
+
&& request.credentialGeneration === confirmation.credential.generation);
|
|
441
|
+
requireValue(await createSha256Hex(encoder.encode(request.bearer), fail) === confirmation.credential.tokenHash);
|
|
442
|
+
return request;
|
|
443
|
+
} catch { return fail(); }
|
|
444
|
+
};
|
|
445
|
+
|
|
398
446
|
/** Reject already expired material; issuance policy and current assignment authority remain server-owned. */
|
|
399
447
|
export const bindGetRuntimeRegistryCredentialResponse = (
|
|
400
448
|
valueArg: unknown, nowArg: number,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import type * as data from '../data/runtimesession.js';
|
|
3
|
+
import type * as binding from '../data/noderuntimebinding.js';
|
|
3
4
|
import type * as terminal from '../data/runtimeterminalreceipt.js';
|
|
4
5
|
import type * as network from '../data/networkdelivery.js';
|
|
5
6
|
import type * as protection from '../data/networkprotection.js';
|
|
@@ -136,6 +137,22 @@ export interface IReq_Pallet_Cloudly_RegisterPalletRuntimeSession extends plugin
|
|
|
136
137
|
response: data.IRegisterPalletRuntimeSessionResponse;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* A local controller's first request on Pallet's root-only Unix socket, before any session:
|
|
142
|
+
* first bind or exact replay of the node's local routing binding (`nodeRuntimeLocalControllerContract`).
|
|
143
|
+
* Pallet answers the exact persisted binding and the credential it will register with, then fires
|
|
144
|
+
* `registerPalletRuntimeSession` at this connection. A node bound to Cloudly refuses it. The
|
|
145
|
+
* response carries a token hash: exclude it from hooks and logs.
|
|
146
|
+
*/
|
|
147
|
+
export interface IReq_Onebox_Pallet_BindLocalRuntimeController extends plugins.typedrequestInterfaces.implementsTR<
|
|
148
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
149
|
+
IReq_Onebox_Pallet_BindLocalRuntimeController
|
|
150
|
+
> {
|
|
151
|
+
method: 'bindPalletLocalRuntimeController';
|
|
152
|
+
request: binding.INodeRuntimeLocalRoutingBinding;
|
|
153
|
+
response: binding.INodeRuntimeLocalBindingConfirmation;
|
|
154
|
+
}
|
|
155
|
+
|
|
139
156
|
/** Controller push over the already authenticated Pallet-initiated connection. */
|
|
140
157
|
export interface IReq_Controller_Pallet_ApplyRuntimeAssignment extends plugins.typedrequestInterfaces.implementsTR<
|
|
141
158
|
plugins.typedrequestInterfaces.ITypedRequest,
|