@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/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,
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '30.6.0',
6
+ version: '30.8.1',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
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
+ });