@serve.zone/interfaces 30.6.0 → 30.8.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 +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/index.d.ts +4 -0
- package/dist_ts/data/index.js +5 -1
- package/dist_ts/data/networkauthority.d.ts +92 -0
- package/dist_ts/data/networkauthority.js +244 -0
- package/dist_ts/data/networkdelivery.d.ts +25 -0
- package/dist_ts/data/networkdelivery.js +38 -0
- package/dist_ts/data/networkprojection.d.ts +140 -0
- package/dist_ts/data/networkprojection.js +371 -0
- package/dist_ts/data/networksigning.d.ts +46 -0
- package/dist_ts/data/networksigning.js +52 -0
- package/dist_ts/private/networkcontract.d.ts +31 -0
- package/dist_ts/private/networkcontract.js +134 -0
- package/dist_ts/requests/runtimesession.d.ts +13 -0
- package/dist_ts/runtime.d.ts +1 -0
- package/dist_ts/runtime.js +2 -1
- package/dist_ts/runtime.networkprojection.d.ts +15 -0
- package/dist_ts/runtime.networkprojection.js +58 -0
- package/package.json +1 -1
- package/readme.md +103 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +4 -0
- package/ts/data/networkauthority.ts +293 -0
- package/ts/data/networkdelivery.ts +58 -0
- package/ts/data/networkprojection.ts +448 -0
- package/ts/data/networksigning.ts +84 -0
- package/ts/private/networkcontract.ts +117 -0
- package/ts/requests/runtimesession.ts +19 -0
- package/ts/runtime.networkprojection.ts +64 -0
- package/ts/runtime.ts +1 -0
package/readme.md
CHANGED
|
@@ -254,6 +254,109 @@ one second. DNS expiry does not prove packet revocation or permit identity reuse
|
|
|
254
254
|
Retirement and attachment responses describe desired or historical metadata;
|
|
255
255
|
consumers must separately track outstanding node revocations and readiness.
|
|
256
256
|
|
|
257
|
+
`IRuntimeNetworkProtectedAuthorityV1` declares the complete controller-owned IPv4
|
|
258
|
+
protection set, disjoint private workload/transit pools, protected resolver and
|
|
259
|
+
platform endpoints, and every affected egress authority, including offline nodes.
|
|
260
|
+
Its snapshot and digest helpers bind the exact predecessor and all declaration
|
|
261
|
+
content. They cannot prove that an operator's inventory is complete. Install the
|
|
262
|
+
union of old and new protection on every affected egress owner, or independently
|
|
263
|
+
fence that owner, before exposing a newly allocatable pool. Removing an entry from
|
|
264
|
+
the declaration is not a revocation acknowledgement. Prefix arrays are sorted
|
|
265
|
+
lexically and nonoverlapping; pools and endpoints are sorted by ID; egress owners
|
|
266
|
+
are ordered by canonical JSON of `[nodeId, runtimeNamespace]`.
|
|
267
|
+
|
|
268
|
+
`IRuntimeNetworkHandoffLeaseV1` binds an immutable node/router incarnation to its
|
|
269
|
+
protected-authority reference, transit subnet and peers, protocol-qualified source
|
|
270
|
+
ports, nonzero conntrack zone and 128-bit label. The label is exactly 32 lowercase
|
|
271
|
+
hex characters. Port ranges are inclusive, sorted by protocol then first port,
|
|
272
|
+
and disjoint within each protocol. `bindRuntimeNetworkHandoffLeaseV1` verifies both
|
|
273
|
+
digests, scope and transit-pool containment and returns detached values. It does
|
|
274
|
+
not authenticate a projection or attest to an actual namespace/interface.
|
|
275
|
+
|
|
276
|
+
`snapshotRuntimeNetworkHandoffLedgerV1` verifies at most 256 retained leases for
|
|
277
|
+
one node/controller epoch, including quarantined allocations. It rejects host
|
|
278
|
+
handoff-port collisions even across router/runtime namespace changes, and zone or
|
|
279
|
+
label reuse within one router incarnation. The caller must transactionally supply
|
|
280
|
+
the complete retained set, serialize allocation and fence controller-epoch changes.
|
|
281
|
+
These are collision checks, not an IPAM allocator or a native capability. No expiry,
|
|
282
|
+
empty conntrack dump, process exit or table removal establishes flow drainage or
|
|
283
|
+
permits handoff identity reuse. Signed complete projections, live interface proofs
|
|
284
|
+
and the separate router/host apply journal remain responsibilities of Cloudly and
|
|
285
|
+
Pallet; neither packet admission nor readiness follows from these helpers.
|
|
286
|
+
|
|
287
|
+
`IRuntimeNetworkWorkloadLeaseV1` is the immutable material referenced by
|
|
288
|
+
`assignment.workload.network`. It binds one execution attempt and router
|
|
289
|
+
incarnation to a workload pool, dedicated unbridged `linkSubnet`, workload
|
|
290
|
+
`address`, exact `/32` `sourcePrefix`, and router-side `gateway`. The gateway
|
|
291
|
+
also supplies `dnsServer` on port 53, because CRI DNS settings cannot encode a
|
|
292
|
+
port. Canonical private point-to-point subnets through `/31` are accepted; the
|
|
293
|
+
owning CNI implementation must qualify its chosen layout. The source prefix,
|
|
294
|
+
link subnet, VPN control prefix and host/router transit subnet have different
|
|
295
|
+
roles. `bindRuntimeNetworkWorkloadLeaseToAssignmentV1` verifies immutable
|
|
296
|
+
assignment identity without a circular assignment digest. Membership, aliases,
|
|
297
|
+
readiness and projection revisions never change this address material.
|
|
298
|
+
|
|
299
|
+
`IRuntimeNetworkProjectionV1` contains complete relevant network definitions,
|
|
300
|
+
service memberships and endpoint leases, including remote ready endpoints and
|
|
301
|
+
declared services with no replicas. It carries the current protected authority,
|
|
302
|
+
exact historical allocation authorities, local handoff, a maximum 15-minute DNS
|
|
303
|
+
window and explicit packet/lease/handoff withdrawals. All arrays use the
|
|
304
|
+
documented canonical ordering: networks and endpoints by ID, memberships by
|
|
305
|
+
canonical `[organizationId, serviceId]`, references by canonical complete
|
|
306
|
+
reference, and withdrawals by canonical complete grant. Projections are bounded
|
|
307
|
+
to 896 KiB, 128 networks, 256 memberships/endpoints, 16 historical authorities
|
|
308
|
+
and 4,096 directed grants. These transport bounds do not assert native capacity;
|
|
309
|
+
the node must preflight the actual SmartVPN, Smartnftables and DNS limits before
|
|
310
|
+
effectful application.
|
|
311
|
+
|
|
312
|
+
`validateRuntimeNetworkProjectionV1` checks complete digest and scope bindings,
|
|
313
|
+
pool containment, disjoint link subnets, network/organization keys, aliases,
|
|
314
|
+
resolver authorization and ready-replica uniqueness. It does not authenticate
|
|
315
|
+
controller inventory, prove observation provenance or authorize allocation reuse.
|
|
316
|
+
Cloudly must derive readiness from the current authenticated assignment report,
|
|
317
|
+
retain all affected offline authorities and complete its allocation/barrier
|
|
318
|
+
transactions before issuance.
|
|
319
|
+
|
|
320
|
+
`getRuntimeNetworkProjectionPacketGrantsV1` derives directed member connectivity
|
|
321
|
+
and explicit local public/platform egress independently of DNS readiness.
|
|
322
|
+
`composeRuntimeNetworkProjectionDnsViewsV1` builds one union view per local
|
|
323
|
+
workload source address, with every authorized FQDN and complete ready-replica A
|
|
324
|
+
set. An empty array declares NODATA; absent/unauthorized names remain NXDOMAIN,
|
|
325
|
+
and IPv4-only service names have AAAA NODATA. The view carries the existing
|
|
326
|
+
forwarding intersection and zero or one search suffix. Pallet must prove the
|
|
327
|
+
actual sandbox/veth/source binding, block private/bare-name forwarding, clip
|
|
328
|
+
TTLs and convert the absolute window using its qualified boot/time authority.
|
|
329
|
+
These helpers neither cache queries nor renew a lease.
|
|
330
|
+
|
|
331
|
+
`admitRuntimeNetworkProjectionV1` checks exact predecessor/replay identity and
|
|
332
|
+
rejects changed same-generation content, regressed membership/readiness evidence,
|
|
333
|
+
changed immutable address/DNS keys, and omitted packet grants without explicit
|
|
334
|
+
withdrawal. Pending withdrawals/tombstones must carry forward until the caller
|
|
335
|
+
supplies the exact previous projection reference from its own durable joined
|
|
336
|
+
application receipt. That reference is never taken from an incoming ACK. Keep
|
|
337
|
+
the accepted history and apply journal until native effects and DNS/allocation
|
|
338
|
+
quarantine are settled; a complete new snapshot does not erase that history.
|
|
339
|
+
|
|
340
|
+
`IRuntimeNetworkSigningAuthorityV1` is separate from human JWT signing. Install
|
|
341
|
+
its current revision through the existing authenticated physical connection and
|
|
342
|
+
commit it with a durable CAS fence. Newly enrolled nodes may bootstrap the
|
|
343
|
+
controller's current key generation; existing trust cannot be reset to do so.
|
|
344
|
+
Exact successor revisions rotate the Ed25519 public key; `publicKey: null`
|
|
345
|
+
revokes it. Rotation/revocation stops acceptance under the previous key, and
|
|
346
|
+
requires newly signed authority for DNS. It does not deny outstanding packets
|
|
347
|
+
or permit allocation reuse.
|
|
348
|
+
|
|
349
|
+
The Node-only `@serve.zone/interfaces/runtime` export supplies
|
|
350
|
+
`createRuntimeNetworkSigningKeyV1`, `signRuntimeNetworkProjectionV1` and
|
|
351
|
+
`verifyRuntimeNetworkProjectionV1`. The signature covers the complete canonical
|
|
352
|
+
projection, signing-authority reference and dedicated signature domain. Verification
|
|
353
|
+
requires receiver-owned trusted authority and node scope; an envelope contains
|
|
354
|
+
no key to trust. Private KeyObjects stay process-local; Cloudly persists their
|
|
355
|
+
exported material only through its encrypted internal secret store. The dedicated
|
|
356
|
+
`requests.runtimesession` key/projection push contracts bind the current physical
|
|
357
|
+
peer. Their response acknowledges durable admission only, never application,
|
|
358
|
+
readiness, independent fencing or pool activation.
|
|
359
|
+
|
|
257
360
|
### Node-Bound Runtime Assignments
|
|
258
361
|
|
|
259
362
|
The `data` namespace exports `IRuntimeAssignmentV1`, canonical digest helpers,
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/index.ts
CHANGED
|
@@ -22,6 +22,10 @@ export * from './isolatedrestore.golden.js';
|
|
|
22
22
|
export * from './mail.js';
|
|
23
23
|
export * from './nodecredential.js';
|
|
24
24
|
export * from './nodeenrollment.js';
|
|
25
|
+
export * from './networkauthority.js';
|
|
26
|
+
export * from './networkdelivery.js';
|
|
27
|
+
export * from './networkprojection.js';
|
|
28
|
+
export * from './networksigning.js';
|
|
25
29
|
export * from './privatenetwork.js';
|
|
26
30
|
export * from './registry.js';
|
|
27
31
|
export * from './runtimeassignment.js';
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import type { TSha256Digest } from './immutableimage.js';
|
|
2
|
+
import type { IRuntimeAuthorityReferenceV1, IRuntimeControllerV1 } from './runtimeassignment.js';
|
|
3
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
4
|
+
|
|
5
|
+
/** Controller declaration; a digest does not authenticate completeness or activate a pool. */
|
|
6
|
+
export interface IRuntimeNetworkProtectedAuthorityV1 {
|
|
7
|
+
schemaVersion: 1;
|
|
8
|
+
id: string;
|
|
9
|
+
generation: number;
|
|
10
|
+
controller: IRuntimeControllerV1;
|
|
11
|
+
previous: IRuntimeAuthorityReferenceV1 | null;
|
|
12
|
+
/** Complete canonical IPv4 union, including all pools and protected endpoints. */
|
|
13
|
+
prefixes: string[];
|
|
14
|
+
pools: Array<{ id: string; purpose: 'workload' | 'transit'; prefix: string }>;
|
|
15
|
+
resolvers: Array<{ id: string; address: string; port: number }>;
|
|
16
|
+
/** Exact underlay exceptions; never inside a managed allocation pool. */
|
|
17
|
+
platformEndpoints: Array<{ id: string; address: string; protocol: 'tcp' | 'udp'; port: number }>;
|
|
18
|
+
/** Includes offline authorities with outstanding grants; omission does not fence them. */
|
|
19
|
+
egressAuthorities: Array<{ nodeId: string; runtimeNamespace: string }>;
|
|
20
|
+
digest: TSha256Digest;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IRuntimeNetworkHandoffPortRangeV1 {
|
|
24
|
+
protocol: 'tcp' | 'udp';
|
|
25
|
+
first: number;
|
|
26
|
+
last: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Immutable allocation, retained unchanged while active or quarantined. No expiry implies reuse. */
|
|
30
|
+
export interface IRuntimeNetworkHandoffLeaseV1 {
|
|
31
|
+
schemaVersion: 1;
|
|
32
|
+
id: string;
|
|
33
|
+
generation: number;
|
|
34
|
+
controller: IRuntimeControllerV1;
|
|
35
|
+
nodeId: string;
|
|
36
|
+
runtimeNamespace: string;
|
|
37
|
+
protectedAuthority: IRuntimeAuthorityReferenceV1;
|
|
38
|
+
/** Logical allocation incarnation, never a path, PID, inode, descriptor or native capability. */
|
|
39
|
+
routerIncarnationId: string;
|
|
40
|
+
transit: { poolId: string; prefix: string; routerAddress: string; hostAddress: string };
|
|
41
|
+
sourcePortRanges: IRuntimeNetworkHandoffPortRangeV1[];
|
|
42
|
+
conntrackZone: number;
|
|
43
|
+
conntrackLabel: string;
|
|
44
|
+
digest: TSha256Digest;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const runtimeNetworkAuthorityV1Contract = Object.freeze({
|
|
48
|
+
protectedDigestDomain: 'serve.zone/runtime-network-protected-authority-v1',
|
|
49
|
+
handoffDigestDomain: 'serve.zone/runtime-network-handoff-lease-v1',
|
|
50
|
+
maximumPrefixes: 128,
|
|
51
|
+
maximumPools: 64,
|
|
52
|
+
maximumResolvers: 16,
|
|
53
|
+
maximumPlatformEndpoints: 64,
|
|
54
|
+
maximumEgressAuthorities: 256,
|
|
55
|
+
maximumPortRanges: 16,
|
|
56
|
+
maximumRetainedHandoffLeases: 256,
|
|
57
|
+
activation: 'all-affected-egress-barriers-or-independent-fences-before-allocation',
|
|
58
|
+
quarantine: 'retain-until-exact-authority-acknowledgements-or-independent-fence',
|
|
59
|
+
omission: 'no-effect',
|
|
60
|
+
} as const);
|
|
61
|
+
|
|
62
|
+
const fail = (): never => { throw new Error('Runtime network authority contract rejected.'); };
|
|
63
|
+
function requireValue(value: unknown): asserts value { if (!value) fail(); }
|
|
64
|
+
const safe = <T>(operation: () => T): T => { try { return operation(); } catch { return fail(); } };
|
|
65
|
+
const record = (value: unknown, keys: string[]): Record<string, unknown> => {
|
|
66
|
+
requireValue(value !== null && typeof value === 'object' && !Array.isArray(value));
|
|
67
|
+
const prototype = Object.getPrototypeOf(value);
|
|
68
|
+
requireValue(prototype === Object.prototype || prototype === null);
|
|
69
|
+
const actual = Reflect.ownKeys(value);
|
|
70
|
+
requireValue(actual.length === keys.length && keys.every(key => actual.includes(key)));
|
|
71
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
74
|
+
requireValue(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
|
|
75
|
+
result[key] = descriptor.value;
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
};
|
|
79
|
+
const list = <T>(value: unknown, maximum: number, read: (item: unknown) => T): T[] => {
|
|
80
|
+
requireValue(Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype);
|
|
81
|
+
const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;
|
|
82
|
+
requireValue(Number.isSafeInteger(length) && length <= maximum && Reflect.ownKeys(value).length === length + 1);
|
|
83
|
+
const result: T[] = [];
|
|
84
|
+
for (let index = 0; index < length; index++) {
|
|
85
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
86
|
+
requireValue(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
|
|
87
|
+
result.push(read(descriptor.value));
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
const id = (value: unknown): string => {
|
|
92
|
+
requireValue(typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(value));
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
const integer = (value: unknown, maximum = Number.MAX_SAFE_INTEGER - 1): number => {
|
|
96
|
+
requireValue(typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= maximum);
|
|
97
|
+
return value;
|
|
98
|
+
};
|
|
99
|
+
const digest = (value: unknown): TSha256Digest => {
|
|
100
|
+
requireValue(typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value));
|
|
101
|
+
return value as TSha256Digest;
|
|
102
|
+
};
|
|
103
|
+
const controller = (value: unknown): IRuntimeControllerV1 => {
|
|
104
|
+
const item = record(value, ['kind', 'id', 'epoch']);
|
|
105
|
+
requireValue(item.kind === 'cloudly' || item.kind === 'onebox');
|
|
106
|
+
return { kind: item.kind, id: id(item.id), epoch: id(item.epoch) };
|
|
107
|
+
};
|
|
108
|
+
const reference = (value: unknown): IRuntimeAuthorityReferenceV1 => {
|
|
109
|
+
const item = record(value, ['id', 'generation', 'digest']);
|
|
110
|
+
return { id: id(item.id), generation: integer(item.generation), digest: digest(item.digest) };
|
|
111
|
+
};
|
|
112
|
+
const protocol = (value: unknown): 'tcp' | 'udp' => {
|
|
113
|
+
requireValue(value === 'tcp' || value === 'udp');
|
|
114
|
+
return value;
|
|
115
|
+
};
|
|
116
|
+
const sorted = <T>(values: T[], key: (value: T) => string): T[] => {
|
|
117
|
+
requireValue(values.every((value, index) => index === 0 || key(values[index - 1]) < key(value)));
|
|
118
|
+
return values;
|
|
119
|
+
};
|
|
120
|
+
const address = (value: unknown): string => {
|
|
121
|
+
requireValue(typeof value === 'string' && value.length <= 15);
|
|
122
|
+
const parts = value.split('.');
|
|
123
|
+
requireValue(parts.length === 4 && parts.every(part => /^(?:0|[1-9][0-9]{0,2})$/.test(part) && Number(part) <= 255));
|
|
124
|
+
return value;
|
|
125
|
+
};
|
|
126
|
+
const numericAddress = (value: string): number => value.split('.').reduce((sum, part) => sum * 256 + Number(part), 0);
|
|
127
|
+
const endpointAddress = (value: unknown): string => {
|
|
128
|
+
const result = address(value), first = Number(result.split('.')[0]);
|
|
129
|
+
requireValue(first > 0 && first < 224 && first !== 127);
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
const prefix = (value: unknown): string => {
|
|
133
|
+
requireValue(typeof value === 'string' && value.length <= 18);
|
|
134
|
+
const parts = value.split('/');
|
|
135
|
+
requireValue(parts.length === 2 && /^(?:[0-9]|[12][0-9]|3[0-2])$/.test(parts[1]));
|
|
136
|
+
const base = numericAddress(address(parts[0])), width = 2 ** (32 - Number(parts[1]));
|
|
137
|
+
requireValue(base % width === 0);
|
|
138
|
+
return value;
|
|
139
|
+
};
|
|
140
|
+
const bounds = (value: string): [number, number] => {
|
|
141
|
+
const [base, bits] = value.split('/'), first = numericAddress(base);
|
|
142
|
+
return [first, first + 2 ** (32 - Number(bits)) - 1];
|
|
143
|
+
};
|
|
144
|
+
const contains = (outer: string, inner: string): boolean => {
|
|
145
|
+
const [a, b] = bounds(outer), [c, d] = bounds(inner);
|
|
146
|
+
return a <= c && b >= d;
|
|
147
|
+
};
|
|
148
|
+
const overlaps = (left: string, right: string): boolean => {
|
|
149
|
+
const [a, b] = bounds(left), [c, d] = bounds(right);
|
|
150
|
+
return a <= d && c <= b;
|
|
151
|
+
};
|
|
152
|
+
const privatePrefix = (value: unknown): string => {
|
|
153
|
+
const result = prefix(value);
|
|
154
|
+
requireValue(['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'].some(range => contains(range, result)));
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
const nonoverlapping = (values: string[]): void => {
|
|
158
|
+
for (let index = 0; index < values.length; index++) for (let other = 0; other < index; other++) {
|
|
159
|
+
requireValue(!overlaps(values[index], values[other]));
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const canonical = (value: unknown): string => canonicalizeStrictJson(value, fail);
|
|
163
|
+
const equals = (left: unknown, right: unknown): boolean => canonical(left) === canonical(right);
|
|
164
|
+
|
|
165
|
+
/** Structural snapshot only. Complete inventory, signatures and activation barriers are caller-owned. */
|
|
166
|
+
export const snapshotRuntimeNetworkProtectedAuthorityV1 = (value: unknown): IRuntimeNetworkProtectedAuthorityV1 => safe(() => {
|
|
167
|
+
const item = record(value, ['schemaVersion', 'id', 'generation', 'controller', 'previous', 'prefixes',
|
|
168
|
+
'pools', 'resolvers', 'platformEndpoints', 'egressAuthorities', 'digest']);
|
|
169
|
+
requireValue(item.schemaVersion === 1);
|
|
170
|
+
const identity = id(item.id), generation = integer(item.generation);
|
|
171
|
+
const previous = item.previous === null ? null : reference(item.previous);
|
|
172
|
+
requireValue(generation === 1 ? previous === null : previous?.id === identity && previous.generation === generation - 1);
|
|
173
|
+
const prefixes = sorted(list(item.prefixes, runtimeNetworkAuthorityV1Contract.maximumPrefixes, prefix), value => value);
|
|
174
|
+
requireValue(prefixes.length > 0);
|
|
175
|
+
nonoverlapping(prefixes);
|
|
176
|
+
const protectedPrefix = (value: string) => requireValue(prefixes.some(range => contains(range, value)));
|
|
177
|
+
const pools = sorted(list(item.pools, runtimeNetworkAuthorityV1Contract.maximumPools, value => {
|
|
178
|
+
const pool = record(value, ['id', 'purpose', 'prefix']);
|
|
179
|
+
requireValue(pool.purpose === 'workload' || pool.purpose === 'transit');
|
|
180
|
+
const result: IRuntimeNetworkProtectedAuthorityV1['pools'][number] = {
|
|
181
|
+
id: id(pool.id), purpose: pool.purpose, prefix: privatePrefix(pool.prefix),
|
|
182
|
+
};
|
|
183
|
+
protectedPrefix(result.prefix);
|
|
184
|
+
return result;
|
|
185
|
+
}), value => value.id);
|
|
186
|
+
nonoverlapping(pools.map(pool => pool.prefix));
|
|
187
|
+
const readEndpoint = (value: unknown, withProtocol: boolean) => {
|
|
188
|
+
const endpoint = record(value, withProtocol ? ['id', 'address', 'protocol', 'port'] : ['id', 'address', 'port']);
|
|
189
|
+
const result = { id: id(endpoint.id), address: endpointAddress(endpoint.address), port: integer(endpoint.port, 65535) };
|
|
190
|
+
protectedPrefix(`${result.address}/32`);
|
|
191
|
+
requireValue(pools.every(pool => !contains(pool.prefix, `${result.address}/32`)));
|
|
192
|
+
return withProtocol ? { ...result, protocol: protocol(endpoint.protocol) } : result;
|
|
193
|
+
};
|
|
194
|
+
const resolvers = sorted(list(item.resolvers, runtimeNetworkAuthorityV1Contract.maximumResolvers,
|
|
195
|
+
value => readEndpoint(value, false)), value => value.id);
|
|
196
|
+
const platformEndpoints = sorted(list(item.platformEndpoints, runtimeNetworkAuthorityV1Contract.maximumPlatformEndpoints,
|
|
197
|
+
value => readEndpoint(value, true) as IRuntimeNetworkProtectedAuthorityV1['platformEndpoints'][number]), value => value.id);
|
|
198
|
+
const allEndpointKeys = [...resolvers.flatMap(value => ['tcp', 'udp'].map(transport => `${value.address}:${value.port}:${transport}`)),
|
|
199
|
+
...platformEndpoints.map(value => `${value.address}:${value.port}:${value.protocol}`)];
|
|
200
|
+
requireValue(new Set(allEndpointKeys).size === allEndpointKeys.length);
|
|
201
|
+
const egressAuthorities = sorted(list(item.egressAuthorities, runtimeNetworkAuthorityV1Contract.maximumEgressAuthorities, value => {
|
|
202
|
+
const authority = record(value, ['nodeId', 'runtimeNamespace']);
|
|
203
|
+
return { nodeId: id(authority.nodeId), runtimeNamespace: id(authority.runtimeNamespace) };
|
|
204
|
+
}), value => canonical([value.nodeId, value.runtimeNamespace]));
|
|
205
|
+
requireValue(egressAuthorities.length > 0);
|
|
206
|
+
return { schemaVersion: 1, id: identity, generation, controller: controller(item.controller), previous,
|
|
207
|
+
prefixes, pools, resolvers, platformEndpoints, egressAuthorities, digest: digest(item.digest) };
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export const snapshotRuntimeNetworkHandoffLeaseV1 = (value: unknown): IRuntimeNetworkHandoffLeaseV1 => safe(() => {
|
|
211
|
+
const item = record(value, ['schemaVersion', 'id', 'generation', 'controller', 'nodeId', 'runtimeNamespace',
|
|
212
|
+
'protectedAuthority', 'routerIncarnationId', 'transit', 'sourcePortRanges', 'conntrackZone', 'conntrackLabel', 'digest']);
|
|
213
|
+
requireValue(item.schemaVersion === 1);
|
|
214
|
+
const transit = record(item.transit, ['poolId', 'prefix', 'routerAddress', 'hostAddress']);
|
|
215
|
+
const transitPrefix = privatePrefix(transit.prefix), [first, last] = bounds(transitPrefix);
|
|
216
|
+
const routerAddress = endpointAddress(transit.routerAddress), hostAddress = endpointAddress(transit.hostAddress);
|
|
217
|
+
requireValue(first !== last && routerAddress !== hostAddress);
|
|
218
|
+
for (const value of [routerAddress, hostAddress]) {
|
|
219
|
+
const number = numericAddress(value);
|
|
220
|
+
requireValue(number >= first && number <= last && (last - first === 1 || (number !== first && number !== last)));
|
|
221
|
+
}
|
|
222
|
+
const sourcePortRanges = list(item.sourcePortRanges, runtimeNetworkAuthorityV1Contract.maximumPortRanges, value => {
|
|
223
|
+
const range = record(value, ['protocol', 'first', 'last']);
|
|
224
|
+
const result = { protocol: protocol(range.protocol), first: integer(range.first, 65535), last: integer(range.last, 65535) };
|
|
225
|
+
requireValue(result.first <= result.last);
|
|
226
|
+
return result;
|
|
227
|
+
});
|
|
228
|
+
requireValue(sourcePortRanges.length > 0);
|
|
229
|
+
for (let index = 1; index < sourcePortRanges.length; index++) {
|
|
230
|
+
const previous = sourcePortRanges[index - 1], current = sourcePortRanges[index];
|
|
231
|
+
requireValue(previous.protocol < current.protocol || (previous.protocol === current.protocol && previous.last < current.first));
|
|
232
|
+
}
|
|
233
|
+
requireValue(typeof item.conntrackLabel === 'string' && /^[a-f0-9]{32}$/.test(item.conntrackLabel)
|
|
234
|
+
&& item.conntrackLabel !== '0'.repeat(32));
|
|
235
|
+
return { schemaVersion: 1, id: id(item.id), generation: integer(item.generation), controller: controller(item.controller),
|
|
236
|
+
nodeId: id(item.nodeId), runtimeNamespace: id(item.runtimeNamespace), protectedAuthority: reference(item.protectedAuthority),
|
|
237
|
+
routerIncarnationId: id(item.routerIncarnationId), transit: { poolId: id(transit.poolId), prefix: transitPrefix, routerAddress, hostAddress },
|
|
238
|
+
sourcePortRanges, conntrackZone: integer(item.conntrackZone, 65535), conntrackLabel: item.conntrackLabel, digest: digest(item.digest) };
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const computeDigest = async (domain: string, value: { digest: TSha256Digest }): Promise<TSha256Digest> => {
|
|
242
|
+
const { digest: omitted, ...payload } = value;
|
|
243
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonical({ domain, payload }), fail)}`;
|
|
244
|
+
};
|
|
245
|
+
export const computeRuntimeNetworkProtectedAuthorityV1Digest = (value: IRuntimeNetworkProtectedAuthorityV1): Promise<TSha256Digest> =>
|
|
246
|
+
computeDigest(runtimeNetworkAuthorityV1Contract.protectedDigestDomain, snapshotRuntimeNetworkProtectedAuthorityV1(value));
|
|
247
|
+
export const computeRuntimeNetworkHandoffLeaseV1Digest = (value: IRuntimeNetworkHandoffLeaseV1): Promise<TSha256Digest> =>
|
|
248
|
+
computeDigest(runtimeNetworkAuthorityV1Contract.handoffDigestDomain, snapshotRuntimeNetworkHandoffLeaseV1(value));
|
|
249
|
+
|
|
250
|
+
/** Detached, digest-verified allocation binding. Does not establish pool activation or live link identity. */
|
|
251
|
+
export const bindRuntimeNetworkHandoffLeaseV1 = async (leaseValue: unknown, authorityValue: unknown): Promise<{
|
|
252
|
+
lease: IRuntimeNetworkHandoffLeaseV1; authority: IRuntimeNetworkProtectedAuthorityV1;
|
|
253
|
+
}> => {
|
|
254
|
+
const lease = snapshotRuntimeNetworkHandoffLeaseV1(leaseValue);
|
|
255
|
+
const authority = snapshotRuntimeNetworkProtectedAuthorityV1(authorityValue);
|
|
256
|
+
requireValue(lease.digest === await computeRuntimeNetworkHandoffLeaseV1Digest(lease));
|
|
257
|
+
requireValue(authority.digest === await computeRuntimeNetworkProtectedAuthorityV1Digest(authority));
|
|
258
|
+
requireValue(equals(lease.controller, authority.controller)
|
|
259
|
+
&& equals(lease.protectedAuthority, { id: authority.id, generation: authority.generation, digest: authority.digest })
|
|
260
|
+
&& authority.egressAuthorities.some(value => value.nodeId === lease.nodeId && value.runtimeNamespace === lease.runtimeNamespace));
|
|
261
|
+
const pool = authority.pools.find(value => value.id === lease.transit.poolId);
|
|
262
|
+
requireValue(pool?.purpose === 'transit' && contains(pool.prefix, lease.transit.prefix));
|
|
263
|
+
return { lease, authority };
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Validate ALL retained leases for one physical node/controller epoch, including quarantine.
|
|
268
|
+
* The owning transactional ledger must supply its complete history and serialize reservations.
|
|
269
|
+
* Namespace replacement, an empty CT dump or process exit never removes an entry from this set.
|
|
270
|
+
*/
|
|
271
|
+
export const snapshotRuntimeNetworkHandoffLedgerV1 = async (value: unknown): Promise<IRuntimeNetworkHandoffLeaseV1[]> => {
|
|
272
|
+
const leases = safe(() => list(value, runtimeNetworkAuthorityV1Contract.maximumRetainedHandoffLeases,
|
|
273
|
+
snapshotRuntimeNetworkHandoffLeaseV1));
|
|
274
|
+
for (let index = 0; index < leases.length; index++) {
|
|
275
|
+
const lease = leases[index];
|
|
276
|
+
requireValue(lease.digest === await computeRuntimeNetworkHandoffLeaseV1Digest(lease));
|
|
277
|
+
for (let other = 0; other < index; other++) {
|
|
278
|
+
const previous = leases[other];
|
|
279
|
+
requireValue(lease.nodeId === previous.nodeId && equals(lease.controller, previous.controller));
|
|
280
|
+
requireValue(lease.id !== previous.id || lease.generation !== previous.generation);
|
|
281
|
+
if (lease.runtimeNamespace === previous.runtimeNamespace && lease.routerIncarnationId === previous.routerIncarnationId) {
|
|
282
|
+
requireValue(lease.conntrackZone !== previous.conntrackZone && lease.conntrackLabel !== previous.conntrackLabel);
|
|
283
|
+
}
|
|
284
|
+
// Host handoff tuples remain reserved even across router or runtime namespace changes.
|
|
285
|
+
if (lease.transit.routerAddress === previous.transit.routerAddress) {
|
|
286
|
+
for (const left of lease.sourcePortRanges) for (const right of previous.sourcePortRanges) {
|
|
287
|
+
requireValue(left.protocol !== right.protocol || left.last < right.first || right.last < left.first);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return leases;
|
|
293
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { IRuntimeAuthorityReferenceV1 } from './runtimeassignment.js';
|
|
2
|
+
import type { IRuntimeSessionBindingV1 } from './runtimesession.js';
|
|
3
|
+
import { snapshotRuntimeSessionBindingV1 } from './runtimesession.js';
|
|
4
|
+
import type { IRuntimeNetworkSigningAuthorityV1, ISignedRuntimeNetworkProjectionV1 } from './networksigning.js';
|
|
5
|
+
import { snapshotRuntimeNetworkSigningAuthorityV1, snapshotSignedRuntimeNetworkProjectionV1 } from './networksigning.js';
|
|
6
|
+
import { captureNetwork, networkEqual, networkRecord, networkReference, networkReferenceOf, networkSafe, requireNetwork } from '../private/networkcontract.js';
|
|
7
|
+
|
|
8
|
+
export interface IApplyRuntimeNetworkSigningAuthorityRequestV1 {
|
|
9
|
+
session: IRuntimeSessionBindingV1;
|
|
10
|
+
authority: IRuntimeNetworkSigningAuthorityV1;
|
|
11
|
+
}
|
|
12
|
+
export interface IApplyRuntimeNetworkProjectionRequestV1 {
|
|
13
|
+
session: IRuntimeSessionBindingV1;
|
|
14
|
+
signed: ISignedRuntimeNetworkProjectionV1;
|
|
15
|
+
}
|
|
16
|
+
/** Durable admission only. No packet, DNS, pool-activation, fencing or readiness assertion. */
|
|
17
|
+
export interface IApplyRuntimeNetworkAuthorityResponseV1 {
|
|
18
|
+
status: 'accepted' | 'replay';
|
|
19
|
+
authority: IRuntimeAuthorityReferenceV1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const snapshotApplyRuntimeNetworkSigningAuthorityRequestV1 = (value: unknown): IApplyRuntimeNetworkSigningAuthorityRequestV1 => networkSafe(() => {
|
|
23
|
+
const item = networkRecord(captureNetwork(value), ['session', 'authority']);
|
|
24
|
+
return { session: snapshotRuntimeSessionBindingV1(item.session), authority: snapshotRuntimeNetworkSigningAuthorityV1(item.authority) };
|
|
25
|
+
});
|
|
26
|
+
export const snapshotApplyRuntimeNetworkProjectionRequestV1 = (value: unknown): IApplyRuntimeNetworkProjectionRequestV1 => networkSafe(() => {
|
|
27
|
+
const item = networkRecord(captureNetwork(value), ['session', 'signed']);
|
|
28
|
+
return { session: snapshotRuntimeSessionBindingV1(item.session), signed: snapshotSignedRuntimeNetworkProjectionV1(item.signed) };
|
|
29
|
+
});
|
|
30
|
+
export const snapshotApplyRuntimeNetworkAuthorityResponseV1 = (value: unknown): IApplyRuntimeNetworkAuthorityResponseV1 => networkSafe(() => {
|
|
31
|
+
const item = networkRecord(captureNetwork(value), ['status', 'authority']);
|
|
32
|
+
requireNetwork(item.status === 'accepted' || item.status === 'replay');
|
|
33
|
+
return { status: item.status, authority: networkReference(item.authority) };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** trustedSessionValue must come from the receiving CURRENT physical connection. */
|
|
37
|
+
export const bindApplyRuntimeNetworkSigningAuthorityRequestV1 = (value: unknown, trustedSessionValue: unknown): IApplyRuntimeNetworkSigningAuthorityRequestV1 => networkSafe(() => {
|
|
38
|
+
const request = snapshotApplyRuntimeNetworkSigningAuthorityRequestV1(value), session = snapshotRuntimeSessionBindingV1(trustedSessionValue);
|
|
39
|
+
requireNetwork(networkEqual(request.session, session) && networkEqual(request.authority.controller, session.controller));
|
|
40
|
+
return request;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Content binding only. Verify signature with stored trust and atomically fence admission. */
|
|
44
|
+
export const bindApplyRuntimeNetworkProjectionRequestV1 = (value: unknown, trustedSessionValue: unknown): IApplyRuntimeNetworkProjectionRequestV1 => networkSafe(() => {
|
|
45
|
+
const request = snapshotApplyRuntimeNetworkProjectionRequestV1(value), session = snapshotRuntimeSessionBindingV1(trustedSessionValue);
|
|
46
|
+
const projection = request.signed.projection;
|
|
47
|
+
requireNetwork(networkEqual(request.session, session) && networkEqual(projection.controller, session.controller)
|
|
48
|
+
&& projection.nodeId === session.nodeId && projection.runtimeNamespace === session.runtimeNamespace);
|
|
49
|
+
return request;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Compare ACK with the exact SENT key or projection reference, never with a response-derived value. */
|
|
53
|
+
export const bindApplyRuntimeNetworkAuthorityResponseV1 = (value: unknown, sentReferenceValue: unknown): IApplyRuntimeNetworkAuthorityResponseV1 => networkSafe(() => {
|
|
54
|
+
const response = snapshotApplyRuntimeNetworkAuthorityResponseV1(value);
|
|
55
|
+
const expected = networkReference(captureNetwork(sentReferenceValue));
|
|
56
|
+
requireNetwork(networkEqual(response.authority, networkReferenceOf(expected)));
|
|
57
|
+
return response;
|
|
58
|
+
});
|