@serve.zone/interfaces 32.42.0 → 32.44.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 +48 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/index.d.ts +2 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/networkegressretirement.d.ts +37 -12
- package/dist_ts/data/networkegressretirement.js +43 -14
- package/dist_ts/data/networkprojection.d.ts +10 -0
- package/dist_ts/data/networkprojection.js +15 -2
- package/dist_ts/data/networkprojectionretirement.d.ts +52 -0
- package/dist_ts/data/networkprojectionretirement.js +33 -0
- package/dist_ts/data/networkprotection.d.ts +29 -1
- package/dist_ts/data/networkprotection.js +45 -1
- package/dist_ts/data/noderetirement.d.ts +3 -1
- package/dist_ts/data/noderetirement.js +1 -1
- package/package.json +3 -3
- package/readme.md +106 -16
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +2 -0
- package/ts/data/networkegressretirement.ts +52 -15
- package/ts/data/networkprojection.ts +20 -1
- package/ts/data/networkprojectionretirement.ts +72 -0
- package/ts/data/networkprotection.ts +52 -1
- package/ts/data/noderetirement.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/interfaces",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.44.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
|
|
6
6
|
"exports": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"@tsclass/tsclass": "^9.5.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@git.zone/tsbuild": "^5.1.
|
|
30
|
+
"@git.zone/tsbuild": "^5.1.2",
|
|
31
31
|
"@git.zone/tsdoc": "^3.0.1",
|
|
32
32
|
"@git.zone/tsrun": "^3.0.2",
|
|
33
|
-
"@git.zone/tstest": "6.3.
|
|
33
|
+
"@git.zone/tstest": "6.3.4",
|
|
34
34
|
"@types/node": "26.6.2"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
package/readme.md
CHANGED
|
@@ -409,7 +409,17 @@ authorities and their digests are validated, and either that does not validate i
|
|
|
409
409
|
protection journal separately from projection admission. It binds a complete
|
|
410
410
|
protected-authority reference, node/controller scope, authenticated reporter,
|
|
411
411
|
boot identity and exact native journal reference to a monotonic receipt chain.
|
|
412
|
-
A null `nativeBarrier` explicitly removes allocation eligibility. A
|
|
412
|
+
A null `nativeBarrier` explicitly removes allocation eligibility. A node sends one
|
|
413
|
+
only in answer to the `retirement` request of a signed projection (see
|
|
414
|
+
[Projection Retirement Requests](#projection-retirement-requests)): its egress
|
|
415
|
+
ownership is being retired with the `withdrawn` disposition, and it mints no
|
|
416
|
+
positive receipt for the chain afterwards
|
|
417
|
+
(`runtimeNetworkProtectionContract.withdrawal`). Nothing else withdraws. The
|
|
418
|
+
node's allocation-pool guard is retained in the kernel when the node process
|
|
419
|
+
ends, and its boot unit restores it before networking starts, so a clean stop, a
|
|
420
|
+
crash, a reboot or being offline leaves the last receipt current. A reconnect
|
|
421
|
+
binds a new reporter session, which the old receipt no longer matches, and the
|
|
422
|
+
node's first positive receipt under that session restores eligibility. A boot or
|
|
413
423
|
protection change requires new native journal evidence. These portable values
|
|
414
424
|
do not inspect a kernel or authenticate their own producer.
|
|
415
425
|
|
|
@@ -682,6 +692,59 @@ reaches only the endpoints it selects. The platform endpoint contract itself
|
|
|
682
692
|
(`{ id, address, protocol, port }`) names no node, which is why this statement is
|
|
683
693
|
per projection.
|
|
684
694
|
|
|
695
|
+
#### Projection Retirement Requests
|
|
696
|
+
|
|
697
|
+
The optional `IRuntimeNetworkProjection.retirement`
|
|
698
|
+
(`IRuntimeNetworkProjectionRetirement { disposition: 'withdrawn', protectedAuthority }`)
|
|
699
|
+
is Cloudly's request that a running node withdraw its allocation protection,
|
|
700
|
+
because its egress ownership is being retired with the `withdrawn` disposition.
|
|
701
|
+
It travels in the projection because that is the one authenticated, monotonic
|
|
702
|
+
channel the node already admits, journals and reads on every pass, and whose
|
|
703
|
+
acknowledged head Cloudly already tracks: the controller's signature covers it, so
|
|
704
|
+
a node cannot invent it; the projection chain is bound to the controller epoch,
|
|
705
|
+
node and runtime namespace, so it cannot be carried elsewhere; and a node's
|
|
706
|
+
acknowledged head proves the node admitted it. A retired node never returns
|
|
707
|
+
(`runtimeNetworkEgressRetirementContract.reentry`), so no later chain of the same
|
|
708
|
+
node can meet a stale request.
|
|
709
|
+
|
|
710
|
+
`protectedAuthority` is the authority current when Cloudly first signed the
|
|
711
|
+
request; its `id` names the receipt chain's authority chain.
|
|
712
|
+
`validateRuntimeNetworkProjection` accepts it only on a `cloudly` projection and
|
|
713
|
+
only naming the projection's own authority chain at a generation the projection
|
|
714
|
+
has reached (the current authority itself at the same generation).
|
|
715
|
+
`admitRuntimeNetworkProjection` latches it: it may first appear on any projection,
|
|
716
|
+
including the first, and that first appearance names exactly the projection's
|
|
717
|
+
current `protectedAuthority` (an older generation is refused, because the node's
|
|
718
|
+
positive receipt under the current authority would then be newer than the request
|
|
719
|
+
and the withdrawal could never complete). Afterwards every successor carries it
|
|
720
|
+
unchanged, never dropped and never rewritten, so it stays fixed while the
|
|
721
|
+
authority chain moves on. Omitted, it keeps the canonical bytes and asks for
|
|
722
|
+
nothing; it grants and withdraws no packet flow. Consumers on interfaces earlier
|
|
723
|
+
than 32.44.0 read projections with exact keys and refuse one that carries
|
|
724
|
+
`retirement`, so Cloudly sends the request only to nodes that report a compatible
|
|
725
|
+
interfaces version.
|
|
726
|
+
`snapshotRuntimeNetworkProjectionRetirement` is the exact-key reader and
|
|
727
|
+
`runtimeNetworkProjectionRetirementContract` states the rules.
|
|
728
|
+
|
|
729
|
+
The node answers with the null-barrier successor of its protection receipt chain
|
|
730
|
+
and mints no positive receipt for that chain again. Its allocation-pool guard
|
|
731
|
+
stays enforced: the answer withdraws eligibility, not protection.
|
|
732
|
+
|
|
733
|
+
`bindRuntimeNetworkProtectionReceiptRetirement(receipt, previous, retirement)` is
|
|
734
|
+
the receiver's gate for every report from a node whose projections carry the
|
|
735
|
+
request, beside `admitRuntimeNetworkProtectionReceipt` with the same `previous`
|
|
736
|
+
(the receiver's current receipt for the node, or null). The receipt must belong to
|
|
737
|
+
the retiring authority chain and `previous`, when given, must be the same chain's
|
|
738
|
+
exact predecessor or the receipt itself. The null answer always passes. A positive
|
|
739
|
+
receipt passes only while it can predate the node's admission of the request, so a
|
|
740
|
+
receipt the node minted before it saw the request still drains from its outbox
|
|
741
|
+
and the withdrawal behind it is never wedged: it names no authority newer than the
|
|
742
|
+
request's (a newer one could only come from a projection already carrying the
|
|
743
|
+
request), and its predecessor is not a null withdrawal (once the receiver holds
|
|
744
|
+
the answer, no positive receipt follows). A positive receipt minted after the
|
|
745
|
+
request against the same authority cannot be told apart from one minted before;
|
|
746
|
+
the node's obligation is never to mint one.
|
|
747
|
+
|
|
685
748
|
`admitRuntimeNetworkProjection` checks exact predecessor/replay identity and
|
|
686
749
|
rejects changed same-generation content, regressed membership/readiness evidence,
|
|
687
750
|
changed immutable address/DNS keys, and omitted packet grants without explicit
|
|
@@ -796,11 +859,15 @@ exact protected authority the owner is about to leave, once all five hold:
|
|
|
796
859
|
(`quarantinedHandoffs`, at most 256, and `quarantinedWorkloads`, at most 512,
|
|
797
860
|
both in canonical order) and is never handed out again;
|
|
798
861
|
5. every remaining owner of that authority acknowledged a projection head that
|
|
799
|
-
carries no endpoint of the node (`clearedProjections`,
|
|
800
|
-
|
|
862
|
+
carries no endpoint of the node (`clearedProjections`, in canonical owner order,
|
|
863
|
+
never the node itself). A remaining owner whose own `lost` fence is recorded
|
|
864
|
+
against the same authority is left out: it never acknowledges anything again, so
|
|
865
|
+
waiting for it would freeze every other owner's retirement, and it leaves behind
|
|
866
|
+
its own fence. Every other remaining owner has exactly one entry.
|
|
801
867
|
|
|
802
868
|
`lastReceipt` is the node's last accepted protection receipt. A `withdrawn` node
|
|
803
|
-
took part in its own retirement, so it always has one (its null withdrawal
|
|
869
|
+
took part in its own retirement, so it always has one (its null withdrawal, the
|
|
870
|
+
answer to its projection's `retirement` request); a
|
|
804
871
|
`lost` node may have none. Neither disposition shortens the fence: a lost node
|
|
805
872
|
leaves behind exactly the same controller-owned evidence as a withdrawn one.
|
|
806
873
|
`snapshotRuntimeNetworkEgressRetirement` is the exact-key reader and
|
|
@@ -810,19 +877,40 @@ evidence; the controller fences the rows it read together with the authority it
|
|
|
810
877
|
stages.
|
|
811
878
|
|
|
812
879
|
`bindRuntimeNetworkProtectedAuthorityRetirement(previous, next, fences,
|
|
813
|
-
retiredOwners)` is the pure successor rule for the egress owners
|
|
814
|
-
space of any step
|
|
880
|
+
retiredOwners, exemptions = [])` is the pure successor rule for the egress owners
|
|
881
|
+
and the address space of any step, and answers `{ previous, next, fences,
|
|
882
|
+
exemptions }`. `next` must be the exact successor of `previous` under the same
|
|
815
883
|
controller, with both digests verified. An owner of `previous` may be absent from
|
|
816
884
|
`next` only when exactly one fence recorded against the exact `previous` reference,
|
|
817
885
|
verified by its digest, names it; a fence for an owner `next` still names, or one
|
|
818
|
-
`previous` never named, refuses the step
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
886
|
+
`previous` never named, refuses the step.
|
|
887
|
+
|
|
888
|
+
`exemptions` are the `lost` fences of owners the step keeps, recorded against the
|
|
889
|
+
same `previous`. They are read and returned, not consumed: each verifies by its
|
|
890
|
+
digest, is recorded under the same controller against the exact `previous`
|
|
891
|
+
reference, and names an owner both `previous` and `next` name that no fence and no
|
|
892
|
+
other exemption names, in canonical owner order and at most 255 per step. A
|
|
893
|
+
withdrawn or stale exemption refuses the step. Every consumed fence's
|
|
894
|
+
`clearedProjections` must name exactly the owners the step keeps less the exempt
|
|
895
|
+
owners, in both directions: a clearance that names an exempt owner or misses a kept
|
|
896
|
+
one refuses the step. An exemption's own clearance is judged only in the step that
|
|
897
|
+
consumes it as a fence. Lost owners can therefore be fenced in parallel against one
|
|
898
|
+
authority and leave 16 per step: each step consumes up to 16 of their fences and
|
|
899
|
+
holds the rest as exemptions, and every later step re-records the remaining fences
|
|
900
|
+
against its own `previous`.
|
|
901
|
+
|
|
902
|
+
`retiredOwners` is the caller's durable history of fenced owners restricted to the
|
|
903
|
+
owners `previous` or `next` names, in canonical order and at most 512 entries. Any
|
|
904
|
+
entry refuses the step, so a fenced owner never returns, and the caller looks up
|
|
905
|
+
only the owners of the two authorities instead of supplying a lifetime list. The
|
|
906
|
+
caller must look up every owner that `previous` and `next` name: the binder cannot
|
|
907
|
+
tell a missed lookup from an empty history, so an omitted entry silently drops the
|
|
908
|
+
guarantee. Every
|
|
909
|
+
pool of `previous` stays unchanged and the prefixes of `next` cover those of
|
|
910
|
+
`previous`; a step that removes an owner also keeps every resolver. Owners may join
|
|
911
|
+
in the same step, and a step without fences is the additive rule, which exemptions
|
|
912
|
+
do not change. One step consumes at most 16 fences
|
|
913
|
+
(`runtimeNetworkEgressRetirementContract`).
|
|
826
914
|
|
|
827
915
|
A retirement step is a new protected authority: every remaining owner reports a
|
|
828
916
|
receipt citing it before anything new is allocated, while existing leases keep
|
|
@@ -1903,8 +1991,10 @@ document. `snapshotNodeRuntimeRetirement` checks the record.
|
|
|
1903
1991
|
|
|
1904
1992
|
`TNodeRuntimeRetirementDisposition` (`nodeRuntimeRetirementDispositions`,
|
|
1905
1993
|
`snapshotNodeRuntimeRetirementDisposition`) says how the node leaves. `withdrawn`: the node still
|
|
1906
|
-
runs and takes part — its slots are removed against its own removal receipts
|
|
1907
|
-
|
|
1994
|
+
runs and takes part — its slots are removed against its own removal receipts, Cloudly asks it
|
|
1995
|
+
through its projection's `retirement` request (see
|
|
1996
|
+
[Projection Retirement Requests](#projection-retirement-requests)), and its Pallet answers by
|
|
1997
|
+
withdrawing its protection receipt before its credentials are revoked. `lost`: an administrator
|
|
1908
1998
|
states the node will never report again, which authorizes abandoning its slots without removal
|
|
1909
1999
|
receipts and nothing else; its egress fence is the same as a withdrawn node's.
|
|
1910
2000
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/index.ts
CHANGED
|
@@ -49,6 +49,8 @@ export { runtimeNetworkHostContract } from './networkhost.js';
|
|
|
49
49
|
export type { IRuntimeNetworkWorkloadIngress, IRuntimeNetworkWorkloadIngressPacketGrant,
|
|
50
50
|
IRuntimeNetworkWorkloadIngressProjection } from './networkworkloadingress.js';
|
|
51
51
|
export { runtimeNetworkWorkloadIngressContract } from './networkworkloadingress.js';
|
|
52
|
+
export type { IRuntimeNetworkProjectionRetirement } from './networkprojectionretirement.js';
|
|
53
|
+
export { runtimeNetworkProjectionRetirementContract, snapshotRuntimeNetworkProjectionRetirement } from './networkprojectionretirement.js';
|
|
52
54
|
export * from './networkprotection.js';
|
|
53
55
|
export * from './networksigning.js';
|
|
54
56
|
export * from './privatenetwork.js';
|
|
@@ -39,10 +39,13 @@ export interface IRuntimeNetworkEgressRetirementClearance extends IRuntimeNetwor
|
|
|
39
39
|
* 4. every handoff and workload lease the node was granted is quarantined (`quarantinedHandoffs`,
|
|
40
40
|
* `quarantinedWorkloads`) and is never handed out again;
|
|
41
41
|
* 5. every remaining owner of `protectedAuthority` acknowledged a projection head that carries no
|
|
42
|
-
* endpoint of the node (`clearedProjections`).
|
|
42
|
+
* endpoint of the node (`clearedProjections`). An owner whose own `lost` fence is recorded
|
|
43
|
+
* against the same authority is not a remaining owner here: it never acknowledges anything again,
|
|
44
|
+
* so it can never clear another owner's endpoints, and it leaves behind its own fence.
|
|
43
45
|
*
|
|
44
46
|
* `lastReceipt` is the node's last accepted protection receipt. A `withdrawn` node took part in its
|
|
45
|
-
* retirement, so it always has one — its null withdrawal
|
|
47
|
+
* retirement, so it always has one — its null withdrawal, the answer to its projection's
|
|
48
|
+
* `retirement` request; a `lost` node may never have reported.
|
|
46
49
|
*
|
|
47
50
|
* A structural snapshot and digest do not authenticate any of it: the controller must fence the
|
|
48
51
|
* rows it read together with the authority it stages.
|
|
@@ -67,7 +70,10 @@ export interface IRuntimeNetworkEgressRetirement extends IRuntimeNetworkNodeScop
|
|
|
67
70
|
quarantinedHandoffs: IRuntimeAuthorityReference[];
|
|
68
71
|
/** Every workload lease the node was granted, each quarantined, in canonical order. */
|
|
69
72
|
quarantinedWorkloads: IRuntimeAuthorityReference[];
|
|
70
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* One entry per remaining owner of `protectedAuthority` whose own `lost` fence is not recorded
|
|
75
|
+
* against that authority, in canonical owner order.
|
|
76
|
+
*/
|
|
71
77
|
clearedProjections: IRuntimeNetworkEgressRetirementClearance[];
|
|
72
78
|
digest: TSha256Digest;
|
|
73
79
|
}
|
|
@@ -79,8 +85,16 @@ export const runtimeNetworkEgressRetirementContract = Object.freeze({
|
|
|
79
85
|
maximumClearedProjections: runtimeNetworkAuthorityContract.maximumEgressAuthorities - 1,
|
|
80
86
|
/** Fences one step may consume. Each is read on its own, so one step stays bounded. */
|
|
81
87
|
maximumFencesPerStep: 16,
|
|
82
|
-
/**
|
|
83
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Lost fences one step may hold as exemptions: owners it keeps that no consumed fence's clearance
|
|
90
|
+
* has to name. At least one owner is kept, so this is one fewer than the owners an authority has.
|
|
91
|
+
*/
|
|
92
|
+
maximumExemptionsPerStep: runtimeNetworkAuthorityContract.maximumEgressAuthorities - 1,
|
|
93
|
+
/**
|
|
94
|
+
* Retired owners one step may be given: the caller's history restricted to owners `previous` or
|
|
95
|
+
* `next` names, which is at most every owner of both.
|
|
96
|
+
*/
|
|
97
|
+
maximumRetiredOwners: 2 * runtimeNetworkAuthorityContract.maximumEgressAuthorities,
|
|
84
98
|
removal: 'absent-owner-requires-fence-bound-to-exact-previous-authority',
|
|
85
99
|
reentry: 'a-fenced-owner-never-returns',
|
|
86
100
|
} as const);
|
|
@@ -132,28 +146,40 @@ export const computeRuntimeNetworkEgressRetirementDigest = (value: IRuntimeNetwo
|
|
|
132
146
|
/**
|
|
133
147
|
* Whether `next` may succeed `previous`, judged on its egress owners and the address space it keeps.
|
|
134
148
|
*
|
|
135
|
-
* Pure and fail-closed. `fences` are the egress retirements this step consumes
|
|
136
|
-
*
|
|
137
|
-
*
|
|
149
|
+
* Pure and fail-closed. `fences` are the egress retirements this step consumes. `exemptions` are
|
|
150
|
+
* `lost` fences of owners this step keeps, recorded against the same `previous`: such an owner never
|
|
151
|
+
* acknowledges another projection head, so no consumed fence's clearance can wait for it. They are
|
|
152
|
+
* read, not consumed, and default to none. `retiredOwners` is the caller's durable history of
|
|
153
|
+
* fenced owners restricted to the owners `previous` or `next` names, in canonical owner order. The
|
|
154
|
+
* step is accepted only when:
|
|
138
155
|
*
|
|
139
156
|
* - `next` is the exact successor of `previous` under the same controller, and both digests verify;
|
|
140
157
|
* - an owner of `previous` is absent from `next` only when exactly one fence, recorded against the
|
|
141
158
|
* exact `previous` reference and verified by its digest, names it — a fence for an owner `next`
|
|
142
159
|
* still names, or for an owner `previous` does not name, refuses the step;
|
|
143
|
-
* - every
|
|
144
|
-
*
|
|
160
|
+
* - every exemption verifies by its digest, is `lost`, is recorded under the same controller against
|
|
161
|
+
* the exact `previous` reference, and names an owner both `previous` and `next` name, no fence
|
|
162
|
+
* names and no other exemption names; at most `maximumExemptionsPerStep`, in canonical owner order;
|
|
163
|
+
* - every fence's `clearedProjections` names exactly the owners of `previous` this step keeps, less
|
|
164
|
+
* the exempt owners; an exemption's own clearance is not judged here, only when its owner leaves;
|
|
165
|
+
* - `retiredOwners` is empty: any entry is either an owner a side names that was fenced before, or
|
|
166
|
+
* not part of the restriction at all, so a fenced owner never returns;
|
|
145
167
|
* - every pool of `previous` is kept unchanged and the prefixes of `next` cover those of `previous`;
|
|
146
168
|
* a step that removes an owner also keeps every resolver of `previous` unchanged.
|
|
147
169
|
*
|
|
148
170
|
* Owners that join in the same step are admitted. With no fence this is the additive owner rule:
|
|
149
|
-
* nobody leaves. It verifies no fence evidence against live state and
|
|
171
|
+
* nobody leaves, and exemptions relax nothing. It verifies no fence evidence against live state and
|
|
172
|
+
* fences no record: the caller fences every fence and exemption it read together with the authority
|
|
173
|
+
* it stages.
|
|
150
174
|
*/
|
|
151
175
|
export const bindRuntimeNetworkProtectedAuthorityRetirement = async (
|
|
152
176
|
previousValue: unknown, nextValue: unknown, fencesValue: unknown, retiredOwnersValue: unknown,
|
|
177
|
+
exemptionsValue: unknown = [],
|
|
153
178
|
): Promise<{
|
|
154
179
|
previous: IRuntimeNetworkProtectedAuthority;
|
|
155
180
|
next: IRuntimeNetworkProtectedAuthority;
|
|
156
181
|
fences: IRuntimeNetworkEgressRetirement[];
|
|
182
|
+
exemptions: IRuntimeNetworkEgressRetirement[];
|
|
157
183
|
}> => {
|
|
158
184
|
try {
|
|
159
185
|
const contract = runtimeNetworkEgressRetirementContract;
|
|
@@ -162,6 +188,9 @@ export const bindRuntimeNetworkProtectedAuthorityRetirement = async (
|
|
|
162
188
|
const elements = readDataElements(fencesValue, contract.maximumFencesPerStep);
|
|
163
189
|
requireNetwork(elements);
|
|
164
190
|
const fences = networkSorted(elements.map(snapshotRuntimeNetworkEgressRetirement), ownerKey);
|
|
191
|
+
const exemptionElements = readDataElements(exemptionsValue, contract.maximumExemptionsPerStep);
|
|
192
|
+
requireNetwork(exemptionElements);
|
|
193
|
+
const exemptions = networkSorted(exemptionElements.map(snapshotRuntimeNetworkEgressRetirement), ownerKey);
|
|
165
194
|
const retiredOwners = networkSorted(networkList(captureNetwork(retiredOwnersValue), contract.maximumRetiredOwners,
|
|
166
195
|
readOwner), ownerKey);
|
|
167
196
|
requireNetwork(previous.digest === await computeRuntimeNetworkProtectedAuthorityDigest(previous)
|
|
@@ -179,17 +208,25 @@ export const bindRuntimeNetworkProtectedAuthorityRetirement = async (
|
|
|
179
208
|
&& previousOwners.has(ownerKey(fence)) && !nextOwners.has(ownerKey(fence)));
|
|
180
209
|
}
|
|
181
210
|
requireNetwork([...previousOwners].every(owner => nextOwners.has(owner) || fenced.has(owner)));
|
|
182
|
-
const
|
|
211
|
+
for (const exemption of exemptions) {
|
|
212
|
+
requireNetwork(exemption.digest === await computeRuntimeNetworkEgressRetirementDigest(exemption)
|
|
213
|
+
&& exemption.disposition === 'lost' && networkEqual(exemption.controller, previous.controller)
|
|
214
|
+
&& networkEqual(exemption.protectedAuthority, reference) && previousOwners.has(ownerKey(exemption))
|
|
215
|
+
&& nextOwners.has(ownerKey(exemption)));
|
|
216
|
+
}
|
|
217
|
+
// A fence's owner is never named by `next`, so no exemption names an owner a fence names.
|
|
218
|
+
const exempt = new Set(exemptions.map(ownerKey));
|
|
219
|
+
const cleared = previous.egressAuthorities.map(ownerKey).filter(owner => !fenced.has(owner) && !exempt.has(owner));
|
|
183
220
|
for (const fence of fences) {
|
|
184
|
-
requireNetwork(networkEqual(fence.clearedProjections.map(ownerKey),
|
|
221
|
+
requireNetwork(networkEqual(fence.clearedProjections.map(ownerKey), cleared));
|
|
185
222
|
}
|
|
186
|
-
requireNetwork(retiredOwners.
|
|
223
|
+
requireNetwork(retiredOwners.length === 0);
|
|
187
224
|
|
|
188
225
|
requireNetwork(previous.pools.every(pool => next.pools.some(candidate => networkEqual(candidate, pool)))
|
|
189
226
|
&& previous.prefixes.every(prefix => networkCoveredBy(next.prefixes, prefix)));
|
|
190
227
|
if (fences.length > 0) {
|
|
191
228
|
requireNetwork(previous.resolvers.every(resolver => next.resolvers.some(candidate => networkEqual(candidate, resolver))));
|
|
192
229
|
}
|
|
193
|
-
return { previous, next, fences };
|
|
230
|
+
return { previous, next, fences, exemptions };
|
|
194
231
|
} catch { return failNetwork(); }
|
|
195
232
|
};
|
|
@@ -11,6 +11,8 @@ import { hostPacketGrantsOf, snapshotHostProjection,
|
|
|
11
11
|
type IRuntimeNetworkHostPacketGrant, type IRuntimeNetworkHostProjection } from './networkhost.js';
|
|
12
12
|
import { snapshotWorkloadIngressProjection, workloadIngressPacketGrantsOf,
|
|
13
13
|
type IRuntimeNetworkWorkloadIngressPacketGrant, type IRuntimeNetworkWorkloadIngressProjection } from './networkworkloadingress.js';
|
|
14
|
+
import { requireFirstProjectionRetirement, requireProjectionRetirement, snapshotRuntimeNetworkProjectionRetirement,
|
|
15
|
+
type IRuntimeNetworkProjectionRetirement } from './networkprojectionretirement.js';
|
|
14
16
|
import { buildPrivateNetworkFqdn, composeServicePrivateNetworkDnsPolicy,
|
|
15
17
|
snapshotPrivateNetwork, snapshotServicePrivateNetworks } from './privatenetwork.js';
|
|
16
18
|
import { captureNetwork, failNetwork, networkCanonical, networkContains, networkCounter, networkDigest,
|
|
@@ -299,6 +301,15 @@ export interface IRuntimeNetworkProjection extends IRuntimeNetworkNodeScope {
|
|
|
299
301
|
* It grants nothing: a workload still reaches only the endpoints it selects.
|
|
300
302
|
*/
|
|
301
303
|
hostPlatformEndpointIds?: string[];
|
|
304
|
+
/**
|
|
305
|
+
* Present once Cloudly retires this node's egress ownership with the `withdrawn` disposition:
|
|
306
|
+
* the node withdraws its allocation protection with the null-barrier successor of its receipt
|
|
307
|
+
* chain and mints no positive receipt for it again (`IRuntimeNetworkProjectionRetirement`). Only
|
|
308
|
+
* a `cloudly` projection may carry it; it first appears naming exactly the projection's current
|
|
309
|
+
* `protectedAuthority`, and every successor carries it unchanged. Omitted, it keeps the canonical
|
|
310
|
+
* bytes and asks for nothing.
|
|
311
|
+
*/
|
|
312
|
+
retirement?: IRuntimeNetworkProjectionRetirement;
|
|
302
313
|
digest: TSha256Digest;
|
|
303
314
|
}
|
|
304
315
|
|
|
@@ -566,10 +577,11 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
566
577
|
const hasWorkloadIngress = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'workloadIngress');
|
|
567
578
|
const hasHostPlatformEndpoints = captured !== null && typeof captured === 'object'
|
|
568
579
|
&& Object.hasOwn(captured, 'hostPlatformEndpointIds');
|
|
580
|
+
const hasRetirement = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'retirement');
|
|
569
581
|
const item = networkRecord(captured, ['id', 'generation', 'previous', 'controller', 'nodeId', 'runtimeNamespace',
|
|
570
582
|
'protectedAuthority', 'allocationAuthorities', 'handoff', 'networks', 'memberships', 'endpoints', 'dns', 'withdrawals', 'retiredLeases', 'retiredHandoffs', 'digest',
|
|
571
583
|
...(hasRouter ? ['router'] : []), ...(hasHost ? ['host'] : []), ...(hasWorkloadIngress ? ['workloadIngress'] : []),
|
|
572
|
-
...(hasHostPlatformEndpoints ? ['hostPlatformEndpointIds'] : [])]);
|
|
584
|
+
...(hasHostPlatformEndpoints ? ['hostPlatformEndpointIds'] : []), ...(hasRetirement ? ['retirement'] : [])]);
|
|
573
585
|
const hostPlatformEndpointIds = hasHostPlatformEndpoints
|
|
574
586
|
? networkSorted(networkList(item.hostPlatformEndpointIds, 64, networkId), value => value) : [];
|
|
575
587
|
requireNetwork(!hasHostPlatformEndpoints || hostPlatformEndpointIds.length > 0);
|
|
@@ -585,6 +597,7 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
585
597
|
...(hasHost ? { host: snapshotHostProjection(item.host) } : {}),
|
|
586
598
|
...(hasWorkloadIngress ? { workloadIngress: snapshotWorkloadIngressProjection(item.workloadIngress) } : {}),
|
|
587
599
|
...(hasHostPlatformEndpoints ? { hostPlatformEndpointIds } : {}),
|
|
600
|
+
...(hasRetirement ? { retirement: snapshotRuntimeNetworkProjectionRetirement(item.retirement) } : {}),
|
|
588
601
|
retiredLeases: sortedReferences(item.retiredLeases), retiredHandoffs: sortedReferences(item.retiredHandoffs), digest: networkDigest(item.digest) };
|
|
589
602
|
});
|
|
590
603
|
|
|
@@ -690,6 +703,7 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
690
703
|
requireNetwork(previous.node !== current.node || previous.last < current.first);
|
|
691
704
|
}
|
|
692
705
|
for (const id of projection.hostPlatformEndpointIds ?? []) requireNetwork(current.platformEndpoints.some(platform => platform.id === id));
|
|
706
|
+
if (projection.retirement) requireProjectionRetirement(projection.retirement, projection.controller.kind, current);
|
|
693
707
|
for (let index = 0; index < projection.endpoints.length; index++) for (let previous = 0; previous < index; previous++) {
|
|
694
708
|
requireNetwork(!networkOverlaps(projection.endpoints[index].lease.ipv4.linkSubnet, projection.endpoints[previous].lease.ipv4.linkSubnet));
|
|
695
709
|
}
|
|
@@ -792,6 +806,7 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
792
806
|
&& (projection.router?.withdrawals.length ?? 0) === 0 && (projection.host?.withdrawals.length ?? 0) === 0
|
|
793
807
|
&& (projection.workloadIngress?.withdrawals.length ?? 0) === 0
|
|
794
808
|
&& projection.retiredLeases.length === 0 && projection.retiredHandoffs.length === 0);
|
|
809
|
+
if (projection.retirement) requireFirstProjectionRetirement(projection.retirement, projection.protectedAuthority);
|
|
795
810
|
return { status: 'accepted', projection };
|
|
796
811
|
}
|
|
797
812
|
const previous = await validateRuntimeNetworkProjection(previousInput);
|
|
@@ -881,6 +896,10 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
881
896
|
}
|
|
882
897
|
if (previous.handoff && !networkEqual(previous.handoff, projection.handoff)) requireNetwork(projection.retiredHandoffs.some(item =>
|
|
883
898
|
networkEqual(item, networkReferenceOf(previous.handoff!))));
|
|
899
|
+
// A retirement request first names the current authority, then is never taken back or
|
|
900
|
+
// rewritten: every successor carries it unchanged.
|
|
901
|
+
if (previous.retirement) requireNetwork(networkEqual(projection.retirement ?? null, previous.retirement));
|
|
902
|
+
else if (projection.retirement) requireFirstProjectionRetirement(projection.retirement, projection.protectedAuthority);
|
|
884
903
|
const protection = projection.protectedAuthority, oldProtection = previous.protectedAuthority;
|
|
885
904
|
requireNetwork(networkEqual(protection, oldProtection) || (protection.id === oldProtection.id
|
|
886
905
|
&& networkEqual(protection.previous, networkReferenceOf(oldProtection))));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { IRuntimeAuthorityReference } from './runtimeassignment.js';
|
|
2
|
+
import type { IRuntimeNetworkProtectedAuthority } from './networkauthority.js';
|
|
3
|
+
import { captureNetwork, networkEqual, networkRecord, networkReference, networkReferenceOf, networkSafe,
|
|
4
|
+
requireNetwork } from '../private/networkcontract.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cloudly's request that a running node withdraw its allocation protection, because its egress
|
|
8
|
+
* ownership is being retired with the `withdrawn` disposition (`INodeRuntimeRetirement`).
|
|
9
|
+
*
|
|
10
|
+
* It travels as `IRuntimeNetworkProjection.retirement`: the one authenticated, monotonic channel a
|
|
11
|
+
* node already admits and journals on every pass. The controller's signature covers it, so a node
|
|
12
|
+
* cannot invent it; the projection chain is bound to the controller epoch, node and runtime
|
|
13
|
+
* namespace, so it cannot be carried to another node or controller; and once a projection carries
|
|
14
|
+
* it, every successor carries it unchanged, so it is never taken back. A retired node never
|
|
15
|
+
* returns (`runtimeNetworkEgressRetirementContract.reentry`): no later chain of the same node can
|
|
16
|
+
* meet a stale signal.
|
|
17
|
+
*
|
|
18
|
+
* The node answers with the null-barrier successor of its protection receipt chain
|
|
19
|
+
* (`IRuntimeNetworkProtectionReceipt.nativeBarrier: null`) and mints no positive receipt for that
|
|
20
|
+
* chain again. Its native allocation-pool guard stays enforced; the answer withdraws eligibility,
|
|
21
|
+
* not protection. Nothing else withdraws: a clean stop, a crash, a reboot or being offline keeps the
|
|
22
|
+
* node's last receipt current.
|
|
23
|
+
*/
|
|
24
|
+
export interface IRuntimeNetworkProjectionRetirement {
|
|
25
|
+
/** The retirement row's disposition. A `lost` node never reports again, so it is never asked. */
|
|
26
|
+
disposition: 'withdrawn';
|
|
27
|
+
/**
|
|
28
|
+
* The protected authority current when Cloudly first signed the request: the projection that
|
|
29
|
+
* first carries it names exactly its own `protectedAuthority`. Its `id` names the authority chain
|
|
30
|
+
* the node's receipts belong to; the value then stays fixed while later projections carry a newer
|
|
31
|
+
* authority of the same chain, never an older one.
|
|
32
|
+
*/
|
|
33
|
+
protectedAuthority: IRuntimeAuthorityReference;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const runtimeNetworkProjectionRetirementContract = Object.freeze({
|
|
37
|
+
controllerKind: 'cloudly',
|
|
38
|
+
disposition: 'withdrawn',
|
|
39
|
+
firstAppearance: 'names-the-current-protected-authority',
|
|
40
|
+
latch: 'every-successor-carries-it-unchanged',
|
|
41
|
+
answer: 'null-native-barrier-receipt-successor',
|
|
42
|
+
positive: 'never-again-for-the-retiring-receipt-chain',
|
|
43
|
+
} as const);
|
|
44
|
+
|
|
45
|
+
/** Exact-key structural snapshot, detached from its input. */
|
|
46
|
+
export const snapshotRuntimeNetworkProjectionRetirement = (value: unknown): IRuntimeNetworkProjectionRetirement => networkSafe(() => {
|
|
47
|
+
const item = networkRecord(captureNetwork(value), ['disposition', 'protectedAuthority']);
|
|
48
|
+
requireNetwork(item.disposition === runtimeNetworkProjectionRetirementContract.disposition);
|
|
49
|
+
return { disposition: item.disposition, protectedAuthority: networkReference(item.protectedAuthority) };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The request belongs to a `cloudly` projection and names the projection's own authority chain at
|
|
54
|
+
* a generation the projection has reached: the current authority itself, or one of its predecessors.
|
|
55
|
+
*/
|
|
56
|
+
export const requireProjectionRetirement = (
|
|
57
|
+
retirement: IRuntimeNetworkProjectionRetirement, controllerKind: string, current: IRuntimeNetworkProtectedAuthority,
|
|
58
|
+
): void => {
|
|
59
|
+
const named = retirement.protectedAuthority;
|
|
60
|
+
requireNetwork(controllerKind === runtimeNetworkProjectionRetirementContract.controllerKind
|
|
61
|
+
&& named.id === current.id && named.generation <= current.generation);
|
|
62
|
+
if (named.generation === current.generation) requireNetwork(networkEqual(named, networkReferenceOf(current)));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The request's first appearance names exactly the projection's current authority. An older
|
|
67
|
+
* generation would make the node's positive receipt under the current authority newer than the
|
|
68
|
+
* request, so the receipt gate refuses it and the withdrawal could never complete.
|
|
69
|
+
*/
|
|
70
|
+
export const requireFirstProjectionRetirement = (
|
|
71
|
+
retirement: IRuntimeNetworkProjectionRetirement, current: IRuntimeNetworkProtectedAuthority,
|
|
72
|
+
): void => requireNetwork(networkEqual(retirement.protectedAuthority, networkReferenceOf(current)));
|
|
@@ -3,6 +3,7 @@ import type { TSha256Digest } from './immutableimage.js';
|
|
|
3
3
|
import type { IRuntimeAuthorityReference } from './runtimeassignment.js';
|
|
4
4
|
import type { IRuntimeNetworkNodeScope } from './networkprojection.js';
|
|
5
5
|
import { snapshotRuntimeNetworkNodeScope } from './networkprojection.js';
|
|
6
|
+
import { snapshotRuntimeNetworkProjectionRetirement } from './networkprojectionretirement.js';
|
|
6
7
|
import type { IRuntimeNetworkProtectedAuthority } from './networkauthority.js';
|
|
7
8
|
import { computeRuntimeNetworkProtectedAuthorityDigest, snapshotRuntimeNetworkProtectedAuthority } from './networkauthority.js';
|
|
8
9
|
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
@@ -16,7 +17,14 @@ import { captureNetwork, failNetwork, networkCanonical, networkDigest, networkEq
|
|
|
16
17
|
* The producer owns a joined native journal covering ALL affected local egress
|
|
17
18
|
* paths. nativeBarrier identifies that complete journal, never an admission ACK,
|
|
18
19
|
* a single table inspection, a boot event or a caller-selected success flag.
|
|
19
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* A null nativeBarrier withdraws this node's allocation eligibility. A node sends it
|
|
22
|
+
* only in answer to a signed projection's `retirement` request
|
|
23
|
+
* (`IRuntimeNetworkProjectionRetirement`), and mints no positive receipt for the chain
|
|
24
|
+
* afterwards. The retained native guard outlives the node process, and the boot unit
|
|
25
|
+
* restores it before networking starts, so a clean stop, a crash, a reboot or being
|
|
26
|
+
* offline withdraws nothing: the last receipt stays current until a reconnect binds
|
|
27
|
+
* a new reporter session, whose own first positive receipt restores eligibility.
|
|
20
28
|
*/
|
|
21
29
|
export interface IRuntimeNetworkProtectionReceipt extends IRuntimeNetworkNodeScope {
|
|
22
30
|
id: string;
|
|
@@ -67,6 +75,7 @@ export const runtimeNetworkProtectionContract = Object.freeze({
|
|
|
67
75
|
allocation: 'all-current-affected-egress-receipts-before-reserving-any-lease',
|
|
68
76
|
receiptSource: 'joined-complete-native-protection-journal',
|
|
69
77
|
unavailable: 'null-native-barrier-removes-allocation-eligibility',
|
|
78
|
+
withdrawal: 'only-in-answer-to-a-signed-projection-retirement',
|
|
70
79
|
historical: 'retain-for-outbox-replay-never-current-allocation-eligibility',
|
|
71
80
|
quarantine: 'no-release-authority',
|
|
72
81
|
maximumHostAddresses: 32,
|
|
@@ -223,3 +232,45 @@ export const bindRuntimeNetworkAllocationProtection = async (
|
|
|
223
232
|
return { protection, receipts };
|
|
224
233
|
} catch { return failNetwork(); }
|
|
225
234
|
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Receiver gate for a protection report from a node whose projection chain carries a retirement
|
|
238
|
+
* request, applied beside `admitRuntimeNetworkProtectionReceipt` with the same `previous`: the
|
|
239
|
+
* receiver's exact current receipt for the node, or null while it holds none. `retirement` is the
|
|
240
|
+
* request the receiver signed into that node's projections.
|
|
241
|
+
*
|
|
242
|
+
* The receipt must belong to the retiring authority chain. A null-barrier receipt is the answer
|
|
243
|
+
* and always passes. A positive receipt passes only while it can predate the node's admission of
|
|
244
|
+
* the request, so a receipt the node minted before it saw the request still drains from its
|
|
245
|
+
* outbox and the withdrawal behind it is never wedged:
|
|
246
|
+
*
|
|
247
|
+
* - it names an authority no newer than the one the request names; a newer one could only come
|
|
248
|
+
* from a projection that already carried the request, since every successor carries it;
|
|
249
|
+
* - its predecessor is not a null withdrawal: once the receiver holds the node's answer, no
|
|
250
|
+
* positive receipt follows it.
|
|
251
|
+
*
|
|
252
|
+
* A positive receipt the node minted after admitting the request against the same authority is
|
|
253
|
+
* indistinguishable from one minted before it; the node's obligation is never to mint one.
|
|
254
|
+
*/
|
|
255
|
+
export const bindRuntimeNetworkProtectionReceiptRetirement = async (
|
|
256
|
+
receiptValue: unknown, previousValue: unknown | null, retirementValue: unknown,
|
|
257
|
+
): Promise<IRuntimeNetworkProtectionReceipt> => {
|
|
258
|
+
try {
|
|
259
|
+
const receipt = snapshotRuntimeNetworkProtectionReceipt(receiptValue);
|
|
260
|
+
const previous = previousValue === null ? null : snapshotRuntimeNetworkProtectionReceipt(previousValue);
|
|
261
|
+
const retirement = snapshotRuntimeNetworkProjectionRetirement(retirementValue);
|
|
262
|
+
requireNetwork(receipt.digest === await computeRuntimeNetworkProtectionReceiptDigest(receipt)
|
|
263
|
+
&& receipt.protectedAuthority.id === retirement.protectedAuthority.id);
|
|
264
|
+
if (previous) {
|
|
265
|
+
requireNetwork(previous.digest === await computeRuntimeNetworkProtectionReceiptDigest(previous)
|
|
266
|
+
&& scopeKey(previous) === scopeKey(receipt) && networkEqual(previous.controller, receipt.controller)
|
|
267
|
+
&& previous.id === receipt.id && (networkEqual(receipt, previous)
|
|
268
|
+
|| networkEqual(receipt.previous, networkReferenceOf(previous))));
|
|
269
|
+
}
|
|
270
|
+
if (receipt.nativeBarrier !== null) {
|
|
271
|
+
requireNetwork(receipt.protectedAuthority.generation <= retirement.protectedAuthority.generation
|
|
272
|
+
&& (previous === null || previous.nativeBarrier !== null));
|
|
273
|
+
}
|
|
274
|
+
return receipt;
|
|
275
|
+
} catch { return failNetwork(); }
|
|
276
|
+
};
|
|
@@ -19,7 +19,9 @@ export const nodeRuntimeRetirementDispositions = Object.freeze(['withdrawn', 'lo
|
|
|
19
19
|
* How the node leaves.
|
|
20
20
|
*
|
|
21
21
|
* `withdrawn`: the node still runs and takes part. Its slots are removed against its own removal
|
|
22
|
-
* receipts,
|
|
22
|
+
* receipts, Cloudly asks it through its projection's `retirement` request
|
|
23
|
+
* (`IRuntimeNetworkProjectionRetirement`), and its Pallet answers by withdrawing its protection
|
|
24
|
+
* receipt before its credentials are revoked.
|
|
23
25
|
*
|
|
24
26
|
* `lost`: an administrator states that the node will never report again — its host is gone, wiped
|
|
25
27
|
* or unreachable for good. Its slots are abandoned without removal receipts, which is the one thing
|