@serve.zone/interfaces 32.19.0 → 32.20.1
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 +44 -0
- package/dist_ts/00_commitinfo_data.js +1 -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 +2 -2
- package/readme.md +71 -1
- package/ts/00_commitinfo_data.ts +1 -1
- 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
|
@@ -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,
|