@serve.zone/interfaces 32.40.1 → 32.42.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 +64 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusterrelaycredential.d.ts +72 -0
- package/dist_ts/data/clusterrelaycredential.js +82 -1
- package/dist_ts/data/corestorecontroltoken.d.ts +90 -0
- package/dist_ts/data/corestorecontroltoken.js +106 -0
- package/dist_ts/data/index.d.ts +2 -0
- package/dist_ts/data/index.js +3 -1
- package/dist_ts/data/networkaddressplan.js +3 -12
- package/dist_ts/data/networkauthority.d.ts +16 -0
- package/dist_ts/data/networkauthority.js +45 -1
- package/dist_ts/data/networkcredential.d.ts +12 -8
- package/dist_ts/data/networkcredential.js +18 -13
- package/dist_ts/data/networkegressretirement.d.ts +105 -0
- package/dist_ts/data/networkegressretirement.js +113 -0
- package/dist_ts/data/noderetirement.d.ts +24 -4
- package/dist_ts/data/noderetirement.js +20 -3
- package/dist_ts/private/networkcontract.d.ts +2 -0
- package/dist_ts/private/networkcontract.js +10 -1
- package/dist_ts/requests/cluster.d.ts +34 -0
- package/dist_ts/requests/corestorecontrol.d.ts +33 -0
- package/dist_ts/requests/corestorecontrol.js +2 -0
- package/dist_ts/requests/index.d.ts +2 -1
- package/dist_ts/requests/index.js +3 -2
- package/dist_ts/requests/node.d.ts +9 -1
- package/package.json +3 -3
- package/readme.md +176 -23
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusterrelaycredential.ts +149 -0
- package/ts/data/corestorecontroltoken.ts +195 -0
- package/ts/data/index.ts +2 -0
- package/ts/data/networkaddressplan.ts +3 -13
- package/ts/data/networkauthority.ts +46 -0
- package/ts/data/networkcredential.ts +17 -12
- package/ts/data/networkegressretirement.ts +195 -0
- package/ts/data/noderetirement.ts +41 -5
- package/ts/private/networkcontract.ts +9 -0
- package/ts/requests/cluster.ts +44 -0
- package/ts/requests/corestorecontrol.ts +43 -0
- package/ts/requests/index.ts +2 -0
- package/ts/requests/node.ts +9 -1
package/readme.md
CHANGED
|
@@ -395,6 +395,16 @@ the declaration is not a revocation acknowledgement. Prefix arrays are sorted
|
|
|
395
395
|
lexically and nonoverlapping; pools and endpoints are sorted by ID; egress owners
|
|
396
396
|
are ordered by canonical JSON of `[nodeId, runtimeNamespace]`.
|
|
397
397
|
|
|
398
|
+
`continuesRuntimeNetworkProtection(previous, next)` states which successor of a protected authority
|
|
399
|
+
only adds. It answers `true` when `next` is `previous` itself, or exactly the next step of its chain
|
|
400
|
+
(the same `id` and controller, `next.previous` naming `previous`) in which every egress owner, pool,
|
|
401
|
+
resolver and platform endpoint of `previous` is still present with the same content and the prefixes
|
|
402
|
+
still cover every address `previous` protected, however they are split. A cluster topology change is
|
|
403
|
+
such a step: a node joins the egress, or a cluster's hub joins the platform endpoints. The step keeps
|
|
404
|
+
its barrier: every egress owner still reports a receipt for `next` before an allocation binds to it.
|
|
405
|
+
Any removal or change, a jump over a step, another chain or another controller answers `false`. Both
|
|
406
|
+
authorities and their digests are validated, and either that does not validate is rejected.
|
|
407
|
+
|
|
398
408
|
`IRuntimeNetworkProtectionReceipt` reports a worker's durable, joined native
|
|
399
409
|
protection journal separately from projection admission. It binds a complete
|
|
400
410
|
protected-authority reference, node/controller scope, authenticated reporter,
|
|
@@ -411,9 +421,11 @@ current persisted protection receipt and durable session for every declared
|
|
|
411
421
|
egress owner, in canonical owner order. Missing, unavailable, stale-protection
|
|
412
422
|
and superseded-session evidence reject. An offline owner cannot be omitted.
|
|
413
423
|
The controller must fence all those records together with lease insertion, before
|
|
414
|
-
reserving any handoff or workload address.
|
|
415
|
-
|
|
416
|
-
|
|
424
|
+
reserving any handoff or workload address. There is no generic independent-fence
|
|
425
|
+
flag, quorum or expiry: the one way an owner leaves is
|
|
426
|
+
[Egress Owner Retirement](#egress-owner-retirement).
|
|
427
|
+
Protection receipts neither prove workload readiness nor release
|
|
428
|
+
packet/address/name quarantine.
|
|
417
429
|
|
|
418
430
|
`IRuntimeNetworkHandoffLease` binds an immutable node/router incarnation to its
|
|
419
431
|
protected-authority reference, transit subnet and peers, protocol-qualified source
|
|
@@ -732,9 +744,12 @@ A credential names one projection, but its key pair belongs to the node's manage
|
|
|
732
744
|
membership, which outlives it. Every workload add, removal, readiness change or policy change
|
|
733
745
|
signs a new projection generation; none of them changes who the member is.
|
|
734
746
|
`continuesRuntimeManagedVpnMembership(previous, next)` is that rule: `next` continues the
|
|
735
|
-
membership when it is the same projection lineage (`id`) under
|
|
736
|
-
the
|
|
737
|
-
|
|
747
|
+
membership when it is the same projection lineage (`id`), under a protected authority that
|
|
748
|
+
continues the previous one (`continuesRuntimeNetworkProtection`: the same authority or its next
|
|
749
|
+
step that only adds), with the same handoff lease (which names the node scope and carries the
|
|
750
|
+
router incarnation), and is `previous` itself or a later generation. A node joining the egress or a
|
|
751
|
+
cluster hub joining the platform endpoints therefore keeps every member's key pair and hub session.
|
|
752
|
+
A protection step that removes or changes anything or skips a step, a fresh handoff (router
|
|
738
753
|
reincarnation) or a new controller epoch is a new membership; an earlier generation or another
|
|
739
754
|
projection of the same generation continues nothing. It validates both projections and rejects
|
|
740
755
|
one that does not validate.
|
|
@@ -761,6 +776,59 @@ Noise implementation owns cryptographic validation and authentication. Credentia
|
|
|
761
776
|
delivery does not prove a connected VPN, TUN ownership, packet enforcement or
|
|
762
777
|
workload readiness, and does not replace explicit packet withdrawals.
|
|
763
778
|
|
|
779
|
+
### Egress Owner Retirement
|
|
780
|
+
|
|
781
|
+
An owner that is offline, revoked or gone can never acknowledge a successor, so
|
|
782
|
+
while it stays declared no allocation is possible after the next protection change.
|
|
783
|
+
`IRuntimeNetworkEgressRetirement` is the independent fence that lets it leave. Its
|
|
784
|
+
owner is the controller, which records it from its own durable state against the
|
|
785
|
+
exact protected authority the owner is about to leave, once all five hold:
|
|
786
|
+
|
|
787
|
+
1. the node carries an `INodeRuntimeRetirement` row, whose `disposition` the fence
|
|
788
|
+
copies;
|
|
789
|
+
2. both node credentials are revoked (`revokedCredentials`, committed at
|
|
790
|
+
`revokedAt`), so the node can no longer register, renew a DNS lease or fetch a
|
|
791
|
+
managed VPN credential;
|
|
792
|
+
3. every DNS and managed VPN window the node was ever issued has ended
|
|
793
|
+
(`packetWindowExpiresAt`, or `revokedAt` when it was issued none), so no hub
|
|
794
|
+
relays for it even if it was never told to stop, and `fencedAt` is not earlier;
|
|
795
|
+
4. every handoff and workload lease the node was granted is quarantined
|
|
796
|
+
(`quarantinedHandoffs`, at most 256, and `quarantinedWorkloads`, at most 512,
|
|
797
|
+
both in canonical order) and is never handed out again;
|
|
798
|
+
5. every remaining owner of that authority acknowledged a projection head that
|
|
799
|
+
carries no endpoint of the node (`clearedProjections`, one per remaining owner
|
|
800
|
+
in canonical owner order, never the node itself).
|
|
801
|
+
|
|
802
|
+
`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); a
|
|
804
|
+
`lost` node may have none. Neither disposition shortens the fence: a lost node
|
|
805
|
+
leaves behind exactly the same controller-owned evidence as a withdrawn one.
|
|
806
|
+
`snapshotRuntimeNetworkEgressRetirement` is the exact-key reader and
|
|
807
|
+
`computeRuntimeNetworkEgressRetirementDigest` seals it under
|
|
808
|
+
`serve.zone/runtime-network-egress-retirement`. Neither authenticates the
|
|
809
|
+
evidence; the controller fences the rows it read together with the authority it
|
|
810
|
+
stages.
|
|
811
|
+
|
|
812
|
+
`bindRuntimeNetworkProtectedAuthorityRetirement(previous, next, fences,
|
|
813
|
+
retiredOwners)` is the pure successor rule for the egress owners and the address
|
|
814
|
+
space of any step. `next` must be the exact successor of `previous` under the same
|
|
815
|
+
controller, with both digests verified. An owner of `previous` may be absent from
|
|
816
|
+
`next` only when exactly one fence recorded against the exact `previous` reference,
|
|
817
|
+
verified by its digest, names it; a fence for an owner `next` still names, or one
|
|
818
|
+
`previous` never named, refuses the step, and every fence's `clearedProjections`
|
|
819
|
+
must name exactly the owners the step keeps. `retiredOwners` is the complete,
|
|
820
|
+
canonically ordered list of owners earlier steps fenced, which the caller's
|
|
821
|
+
durable history supplies; neither side may name any of them, so a fenced owner
|
|
822
|
+
never returns. Every pool of `previous` stays unchanged and the prefixes of `next`
|
|
823
|
+
cover those of `previous`; a step that removes an owner also keeps every resolver.
|
|
824
|
+
Owners may join in the same step, and a step without fences is the additive rule.
|
|
825
|
+
One step consumes at most 16 fences (`runtimeNetworkEgressRetirementContract`).
|
|
826
|
+
|
|
827
|
+
A retirement step is a new protected authority: every remaining owner reports a
|
|
828
|
+
receipt citing it before anything new is allocated, while existing leases keep
|
|
829
|
+
their historical eligibility. It also ends every managed VPN membership, as every
|
|
830
|
+
protection change does, so each retirement costs the fleet one reconnect.
|
|
831
|
+
|
|
764
832
|
### DNS Lease Renewals
|
|
765
833
|
|
|
766
834
|
A projection's `dns` window lasts at most fifteen minutes and is signed content, so a node's
|
|
@@ -1822,20 +1890,33 @@ status or assignments changed; it carries no state, so a late push never overwri
|
|
|
1822
1890
|
|
|
1823
1891
|
### Node Retirement
|
|
1824
1892
|
|
|
1825
|
-
`data.INodeRuntimeRetirement { id, clusterId, state, actorId, since }` records that one
|
|
1826
|
-
its cluster's Pallet runtime. `TNodeRuntimeRetirementState` is `retiring` while the
|
|
1827
|
-
stops and removes the node's slots, the compiler quarantines its handoff and
|
|
1828
|
-
are revoked
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
`
|
|
1838
|
-
|
|
1893
|
+
`data.INodeRuntimeRetirement { id, clusterId, state, disposition, actorId, since }` records that one
|
|
1894
|
+
node leaves its cluster's Pallet runtime. `TNodeRuntimeRetirementState` is `retiring` while the
|
|
1895
|
+
producer still stops and removes the node's slots, the compiler quarantines its handoff and leases,
|
|
1896
|
+
both node credentials are revoked and the node's egress fence is not yet recorded, and `retired`
|
|
1897
|
+
once the protected authority no longer names the node (see
|
|
1898
|
+
[Egress Owner Retirement](#egress-owner-retirement)). It is stored beside the node document, because
|
|
1899
|
+
a node update merges caller data and no caller may retire a node with it. The producers read the
|
|
1900
|
+
row's presence rather than its state: a node that has one is no longer an egress member and desires
|
|
1901
|
+
no slots, so the row is written before anything is taken away and stays as long as the node
|
|
1902
|
+
document. `snapshotNodeRuntimeRetirement` checks the record.
|
|
1903
|
+
|
|
1904
|
+
`TNodeRuntimeRetirementDisposition` (`nodeRuntimeRetirementDispositions`,
|
|
1905
|
+
`snapshotNodeRuntimeRetirementDisposition`) says how the node leaves. `withdrawn`: the node still
|
|
1906
|
+
runs and takes part — its slots are removed against its own removal receipts and its Pallet
|
|
1907
|
+
withdraws its protection receipt before its credentials are revoked. `lost`: an administrator
|
|
1908
|
+
states the node will never report again, which authorizes abandoning its slots without removal
|
|
1909
|
+
receipts and nothing else; its egress fence is the same as a withdrawn node's.
|
|
1910
|
+
|
|
1911
|
+
`requests.node.retireClusterNode { identity, nodeId, expectedGeneration, disposition? }` answers
|
|
1912
|
+
`{ retirement }`. `expectedGeneration` is the cluster runtime generation the caller read with
|
|
1913
|
+
`getClusterRuntime`, so a cutover that moved the phase meanwhile refuses the write. A request that
|
|
1914
|
+
states no disposition asks for `withdrawn`, the retirement this request always described; `lost`
|
|
1915
|
+
is taken only when stated, and asking again as `lost` escalates a `withdrawn` retirement that is
|
|
1916
|
+
still `retiring`. Every refusal is one of `data.nodeRuntimeRetirementRefusals` — `node-missing`,
|
|
1917
|
+
`cluster-runtime-not-pallet`, `node-already-retired`, `conflict` or `node-still-reporting` (a `lost`
|
|
1918
|
+
retirement of a node whose last accepted report is younger than
|
|
1919
|
+
`clusterNodeObservationFreshnessMs`) — and takes nothing away.
|
|
1839
1920
|
|
|
1840
1921
|
### Corestore Database Export Packaging
|
|
1841
1922
|
|
|
@@ -2723,8 +2804,9 @@ cluster authority every cluster-side component used before it. `IClusterRelayCre
|
|
|
2723
2804
|
metadata: the cluster, a `generation` that advances by one per accepted mint or rotation, the
|
|
2724
2805
|
machine `userId` it authenticates as, and `delivery`, the `ISecretVersionReference` of the
|
|
2725
2806
|
immutable encrypted version the bearer was delivered as. No record, receipt, request or response
|
|
2726
|
-
of this contract carries a bearer, a hash or key material,
|
|
2727
|
-
|
|
2807
|
+
of this contract carries a bearer, a hash or key material, except the one answer that exists to
|
|
2808
|
+
hand the bearer over (see the export below), and every reader is exact-keyed, so a peer that adds
|
|
2809
|
+
a secret-looking member is refused rather than copied.
|
|
2728
2810
|
|
|
2729
2811
|
The credential authenticates the cluster's whole control-plane session, exactly what the shared
|
|
2730
2812
|
authority authenticated, so a relay changes one credential and keeps every call it makes. The gain
|
|
@@ -2776,7 +2858,36 @@ it names or that record's revoked successor — and to a registration whose `cre
|
|
|
2776
2858
|
invalidation's `replacement`. It is a content check; dereferencing each record as current and
|
|
2777
2859
|
requiring the live socket remain Cloudly's.
|
|
2778
2860
|
|
|
2779
|
-
|
|
2861
|
+
`exportClusterRelayAuthorization { identity, clusterId, expectedGeneration }` answers the bearer
|
|
2862
|
+
itself, so an operator hands it to the relay without an exec into Cloudly's process. Cloudly mints
|
|
2863
|
+
nothing to answer it: the bearer is the encrypted version the credential's `delivery` names, read
|
|
2864
|
+
at exactly `expectedGeneration` (never a minimum) and proved against the hash that generation
|
|
2865
|
+
issued, so a rotation committing meanwhile is refused rather than answered. A new bearer is a
|
|
2866
|
+
`rotate` followed by an export at the new generation. It requires a freshly authenticated
|
|
2867
|
+
administrator, exactly as a credential mutation does. `IClusterRelayAuthorizationExport` carries
|
|
2868
|
+
the sealed `credential` at that generation, the `authorization` (the bearer, byte for byte what the
|
|
2869
|
+
relay presents: `clusterRelayToken_<credential id>_<64 hex>`, no line terminator) and the
|
|
2870
|
+
`receipt`. It is the only answer of this contract that carries a bearer; Cloudly never logs it, and
|
|
2871
|
+
its reader writes it where the relay reads it and nowhere else.
|
|
2872
|
+
|
|
2873
|
+
Every answered export is audited: Cloudly writes `IClusterRelayAuthorizationExportReceipt`
|
|
2874
|
+
(`id`, `clusterId`, the exact `credential` reference, the administrator's `actorId`,
|
|
2875
|
+
`exportedAt`) before it answers, so no bearer leaves without a record, and a retried export is a
|
|
2876
|
+
second receipt rather than a replay. A receipt is value-free: no bearer, no hash.
|
|
2877
|
+
`getClusterRelayAuthorizationExports { identity, clusterId }` answers a cluster's receipts, oldest
|
|
2878
|
+
first. Refusals are `data.clusterRelayAuthorizationExportRefusals`
|
|
2879
|
+
(`data.TClusterRelayAuthorizationExportRefusal`): `relay-credential-absent`,
|
|
2880
|
+
`relay-credential-generation-mismatch`, `relay-authorization-missing` and
|
|
2881
|
+
`relay-authorization-ambiguous`. A reader checks an answer with
|
|
2882
|
+
`bindClusterRelayAuthorizationExport(answer, { clusterId, expectedGeneration })` before it writes a
|
|
2883
|
+
byte: the credential's digest holds, it is the requested cluster's at the requested generation, the
|
|
2884
|
+
bearer names that credential, and the receipt names that exact credential. Every refusal is the
|
|
2885
|
+
contract's one value-free message and never quotes the answer. `isClusterRelayAuthorization`
|
|
2886
|
+
states the bearer's shape, and `snapshotClusterRelayAuthorizationExportRequest`,
|
|
2887
|
+
`snapshotClusterRelayAuthorizationExportReceipt` and `snapshotClusterRelayAuthorizationExport` are
|
|
2888
|
+
the exact-keyed readers.
|
|
2889
|
+
|
|
2890
|
+
All five requests read only the verified `IIdentityCredential`, never an identity member a caller
|
|
2780
2891
|
states about itself.
|
|
2781
2892
|
|
|
2782
2893
|
### Spark Legacy Runtime Retirement
|
|
@@ -3581,6 +3692,7 @@ Request groups are exported by product area:
|
|
|
3581
3692
|
- `requests.coremail`
|
|
3582
3693
|
- `requests.coremailAdmin`
|
|
3583
3694
|
- `requests.corestore`
|
|
3695
|
+
- `requests.corestorecontrol`
|
|
3584
3696
|
- `requests.deployment`
|
|
3585
3697
|
- `requests.dns`
|
|
3586
3698
|
- `requests.domain`
|
|
@@ -3694,6 +3806,47 @@ bytes and digests exactly.
|
|
|
3694
3806
|
payload, one-byte-larger framed snapshot-original, 128 MiB verified closure
|
|
3695
3807
|
plaintext, and 256 MiB closure boundaries used by Corestore.
|
|
3696
3808
|
|
|
3809
|
+
### Corestore Control Token Administration
|
|
3810
|
+
|
|
3811
|
+
The elected relay of every cluster calls its nodes' Corestore control API with one control token
|
|
3812
|
+
(`CORESTORE_API_TOKEN`), which Cloudly stores for both Corestore provider configs and hands to the
|
|
3813
|
+
relay sealed (see Corestore Runtime Credentials). `requests.corestorecontrol` lets an administrator
|
|
3814
|
+
store that token without an exec into Cloudly's process.
|
|
3815
|
+
|
|
3816
|
+
`setCorestoreControlToken { identity, token, expected }` stores `token` under both provider configs
|
|
3817
|
+
in one transaction, so either both hold it or neither does. `token` must pass
|
|
3818
|
+
`isCorestoreControlToken` (32 to 4096 UTF-8 bytes, no whitespace) and is the only member of this
|
|
3819
|
+
contract that carries the token: it is never in an answer, a receipt, a log or a hook, and a sender
|
|
3820
|
+
never persists the request. `expected` is the `data.TCorestoreControlTokenState` the administrator
|
|
3821
|
+
last read: `{ state: 'absent' }` or `{ state: 'present', revision }`, never a minimum, so a set that
|
|
3822
|
+
committed in between is refused rather than overwritten. `revision` is 1 for the first token ever
|
|
3823
|
+
stored and advances by exactly one with every accepted set; no request removes a token. It requires
|
|
3824
|
+
a freshly authenticated administrator, exactly as a relay credential mutation does. The answer,
|
|
3825
|
+
`data.ICorestoreControlTokenSetResult`, is the state the set produced and its receipt. Refusals are
|
|
3826
|
+
`data.corestoreControlTokenSetRefusals` (`data.TCorestoreControlTokenSetRefusal`):
|
|
3827
|
+
`token-invalid`, `token-present`, `token-absent`, `token-revision-mismatch` and
|
|
3828
|
+
`provider-unavailable`.
|
|
3829
|
+
|
|
3830
|
+
Every accepted set is audited: Cloudly writes `data.ICorestoreControlTokenSetReceipt` (`id`,
|
|
3831
|
+
`revision`, `via`, `actorId`, `setAt`) in the transaction that stores the token. `via` is
|
|
3832
|
+
`admin-request` with the administrator's `actorId`, or `container-cli` with
|
|
3833
|
+
`data.corestoreControlTokenContainerCliActorId` (`operator:secrets-cli`) when Cloudly's in-container
|
|
3834
|
+
`secrets set-corestore-control-token` stored it; that command advances the same revision. A receipt
|
|
3835
|
+
is value-free: no token, no hash. `getCorestoreControlTokenState { identity }` answers
|
|
3836
|
+
`data.ICorestoreControlTokenStateRead`: the state and its receipts, oldest first, contiguous and
|
|
3837
|
+
ending at the state's revision; no token stored means no receipt. Only a token stored before
|
|
3838
|
+
receipts existed, which Cloudly's migration records as revision 1, has no receipt, so the receipts
|
|
3839
|
+
start at revision 1 or 2. A read answers at most `data.corestoreControlTokenMaximumReceipts` (4096)
|
|
3840
|
+
receipts: past that many sets it answers the latest 4096, whose oldest may name any revision.
|
|
3841
|
+
|
|
3842
|
+
A reader checks a set answer with `bindCorestoreControlTokenSet(answer, request)`: the state is
|
|
3843
|
+
present at revision 1 after an absent expectation and at the expected revision plus one otherwise,
|
|
3844
|
+
and the receipt is an `admin-request` receipt for exactly that revision.
|
|
3845
|
+
`snapshotCorestoreControlTokenSetRequest`, `snapshotCorestoreControlTokenSetResult`,
|
|
3846
|
+
`snapshotCorestoreControlTokenSetReceipt`, `snapshotCorestoreControlTokenState` and
|
|
3847
|
+
`snapshotCorestoreControlTokenStateRead` are the exact-keyed readers. Every refusal is the
|
|
3848
|
+
contract's one value-free message and never quotes the token.
|
|
3849
|
+
|
|
3697
3850
|
### Corestore Through The Relay
|
|
3698
3851
|
|
|
3699
3852
|
A serve.zone cluster is outbound-only and Corestore is node-local, so Cloudly never reaches a
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -128,6 +128,53 @@ export interface IClusterLegacyRelayCredentialRetirementResult {
|
|
|
128
128
|
replayed: boolean;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* What an administrator names to export one cluster's relay bearer: the cluster, at exactly the
|
|
133
|
+
* generation `getClusterRelayCredential` answered. Never a minimum: a rotation that committed
|
|
134
|
+
* after that read is refused rather than answered with a bearer the operator did not ask for.
|
|
135
|
+
*/
|
|
136
|
+
export interface IClusterRelayAuthorizationExportRequest {
|
|
137
|
+
clusterId: string;
|
|
138
|
+
expectedGeneration: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The audit record of one answered export, which Cloudly writes before it answers the bearer.
|
|
143
|
+
*
|
|
144
|
+
* Append-only and value-free: it names who received which credential's bearer and when, never
|
|
145
|
+
* the bearer or a hash of it. Every answered export writes one, so a retried export is a second
|
|
146
|
+
* receipt rather than a replay: each is a separate handover of the same bearer.
|
|
147
|
+
*/
|
|
148
|
+
export interface IClusterRelayAuthorizationExportReceipt {
|
|
149
|
+
id: string;
|
|
150
|
+
clusterId: string;
|
|
151
|
+
/** The exact relay credential whose bearer was answered: identity, generation and digest. */
|
|
152
|
+
credential: IRuntimeAuthorityReference;
|
|
153
|
+
/** The administrator the bearer was answered to, read from the verified credential. */
|
|
154
|
+
actorId: string;
|
|
155
|
+
exportedAt: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* One cluster's relay bearer, answered to an administrator so it can be handed to the relay
|
|
160
|
+
* without reaching into Cloudly's process.
|
|
161
|
+
*
|
|
162
|
+
* The only member of this contract that carries a bearer. It is the delivered version the
|
|
163
|
+
* credential names, at the requested generation, proved against the hash that generation issued;
|
|
164
|
+
* Cloudly mints nothing to answer it. The reader owns the bytes: it writes them where the relay
|
|
165
|
+
* reads them and never logs, prints or persists them anywhere else.
|
|
166
|
+
*/
|
|
167
|
+
export interface IClusterRelayAuthorizationExport {
|
|
168
|
+
/** The relay credential the bearer authenticates as, at exactly the requested generation. */
|
|
169
|
+
credential: IClusterRelayCredential;
|
|
170
|
+
/**
|
|
171
|
+
* The bearer, byte for byte what the relay presents: `clusterRelayToken_`, the credential's
|
|
172
|
+
* 64-hex `id`, `_` and 64 hex characters, with no line terminator.
|
|
173
|
+
*/
|
|
174
|
+
authorization: string;
|
|
175
|
+
receipt: IClusterRelayAuthorizationExportReceipt;
|
|
176
|
+
}
|
|
177
|
+
|
|
131
178
|
export const clusterRelayCredentialContract = Object.freeze({
|
|
132
179
|
credentialDigestDomain: 'serve.zone/cluster-relay-credential',
|
|
133
180
|
legacyCredentialDigestDomain: 'serve.zone/cluster-relay-legacy-credential',
|
|
@@ -154,6 +201,24 @@ export const clusterRelayCredentialRefusals = Object.freeze([
|
|
|
154
201
|
|
|
155
202
|
export type TClusterRelayCredentialRefusal = typeof clusterRelayCredentialRefusals[number];
|
|
156
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Every reason Cloudly answers instead of a relay bearer. Its own family, because an export
|
|
206
|
+
* changes no record: it has no `conflict`, and a generation it was not asked for is a mismatch.
|
|
207
|
+
*/
|
|
208
|
+
export const clusterRelayAuthorizationExportRefusals = Object.freeze([
|
|
209
|
+
/** No relay credential is minted for the cluster. */
|
|
210
|
+
'relay-credential-absent',
|
|
211
|
+
/** The credential is at another generation than the one named, before the read or after it. */
|
|
212
|
+
'relay-credential-generation-mismatch',
|
|
213
|
+
/** The version the credential delivered is not stored as an active version. */
|
|
214
|
+
'relay-authorization-missing',
|
|
215
|
+
/** The secret's active version, or its bytes, are not what the credential issued. */
|
|
216
|
+
'relay-authorization-ambiguous',
|
|
217
|
+
] as const);
|
|
218
|
+
|
|
219
|
+
export type TClusterRelayAuthorizationExportRefusal =
|
|
220
|
+
typeof clusterRelayAuthorizationExportRefusals[number];
|
|
221
|
+
|
|
157
222
|
const fail = (): never => { throw new Error('Cluster relay credential contract rejected.'); };
|
|
158
223
|
const safe = <T>(operationArg: () => T): T => {
|
|
159
224
|
try { return operationArg(); } catch { return fail(); }
|
|
@@ -359,3 +424,87 @@ export const bindClusterLegacyRelayCredentialInvalidation = async (
|
|
|
359
424
|
return { invalidation, legacyCredential, acceptedRegistration };
|
|
360
425
|
} catch { return fail(); }
|
|
361
426
|
};
|
|
427
|
+
|
|
428
|
+
const clusterRelayAuthorizationPrefix = 'clusterRelayToken_';
|
|
429
|
+
const clusterRelayAuthorizationPattern = /^clusterRelayToken_[a-f0-9]{64}_[a-f0-9]{64}$/;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Whether a value has the exact shape of a relay bearer: `clusterRelayToken_<64 hex>_<64 hex>`.
|
|
433
|
+
*
|
|
434
|
+
* Shape only: whether it authenticates is Cloudly's to decide against the hash it keeps.
|
|
435
|
+
*/
|
|
436
|
+
export const isClusterRelayAuthorization = (valueArg: unknown): valueArg is string =>
|
|
437
|
+
typeof valueArg === 'string' && clusterRelayAuthorizationPattern.test(valueArg);
|
|
438
|
+
|
|
439
|
+
/** The credential identity a relay bearer names: the 64 hex characters after its prefix. */
|
|
440
|
+
const clusterRelayAuthorizationCredentialId = (authorizationArg: string): string =>
|
|
441
|
+
authorizationArg.slice(
|
|
442
|
+
clusterRelayAuthorizationPrefix.length,
|
|
443
|
+
clusterRelayAuthorizationPrefix.length + 64,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
export const snapshotClusterRelayAuthorizationExportRequest = (
|
|
447
|
+
valueArg: unknown,
|
|
448
|
+
): IClusterRelayAuthorizationExportRequest => safe(() => {
|
|
449
|
+
const value = networkRecord(captureNetwork(valueArg), ['clusterId', 'expectedGeneration']);
|
|
450
|
+
return {
|
|
451
|
+
clusterId: networkId(value.clusterId),
|
|
452
|
+
expectedGeneration: networkCounter(value.expectedGeneration),
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
export const snapshotClusterRelayAuthorizationExportReceipt = (
|
|
457
|
+
valueArg: unknown,
|
|
458
|
+
): IClusterRelayAuthorizationExportReceipt => safe(() => {
|
|
459
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
460
|
+
'id', 'clusterId', 'credential', 'actorId', 'exportedAt',
|
|
461
|
+
]);
|
|
462
|
+
return {
|
|
463
|
+
id: networkId(value.id),
|
|
464
|
+
clusterId: networkId(value.clusterId),
|
|
465
|
+
credential: networkReference(value.credential),
|
|
466
|
+
actorId: networkId(value.actorId),
|
|
467
|
+
exportedAt: networkCounter(value.exportedAt),
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Structural copy of an export answer. A refusal never quotes the answer, because it carries a
|
|
473
|
+
* bearer: every failure is the contract's one value-free message.
|
|
474
|
+
*/
|
|
475
|
+
export const snapshotClusterRelayAuthorizationExport = (
|
|
476
|
+
valueArg: unknown,
|
|
477
|
+
): IClusterRelayAuthorizationExport => safe(() => {
|
|
478
|
+
const value = networkRecord(captureNetwork(valueArg), ['credential', 'authorization', 'receipt']);
|
|
479
|
+
requireNetwork(isClusterRelayAuthorization(value.authorization));
|
|
480
|
+
return {
|
|
481
|
+
credential: snapshotClusterRelayCredential(value.credential),
|
|
482
|
+
authorization: value.authorization,
|
|
483
|
+
receipt: snapshotClusterRelayAuthorizationExportReceipt(value.receipt),
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Joins an export answer to the request it answers, before a reader writes a byte of the bearer.
|
|
489
|
+
*
|
|
490
|
+
* The credential is sealed with its own digest and is the requested cluster's at exactly the
|
|
491
|
+
* requested generation; the bearer names that credential; the receipt names that cluster and that
|
|
492
|
+
* exact credential. Content join only: whether the bearer authenticates is Cloudly's to prove,
|
|
493
|
+
* and Cloudly proves it before it answers.
|
|
494
|
+
*/
|
|
495
|
+
export const bindClusterRelayAuthorizationExport = async (
|
|
496
|
+
exportArg: unknown,
|
|
497
|
+
requestArg: unknown,
|
|
498
|
+
): Promise<IClusterRelayAuthorizationExport> => {
|
|
499
|
+
try {
|
|
500
|
+
const request = snapshotClusterRelayAuthorizationExportRequest(requestArg);
|
|
501
|
+
const answer = snapshotClusterRelayAuthorizationExport(exportArg);
|
|
502
|
+
if ((await validateClusterRelayCredential(answer.credential)).length) fail();
|
|
503
|
+
requireNetwork(answer.credential.clusterId === request.clusterId
|
|
504
|
+
&& answer.credential.generation === request.expectedGeneration
|
|
505
|
+
&& clusterRelayAuthorizationCredentialId(answer.authorization) === answer.credential.id
|
|
506
|
+
&& answer.receipt.clusterId === request.clusterId
|
|
507
|
+
&& networkEqual(answer.receipt.credential, networkReferenceOf(answer.credential)));
|
|
508
|
+
return answer;
|
|
509
|
+
} catch { return fail(); }
|
|
510
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { isCorestoreControlToken } from '../runtime.corestore.js';
|
|
2
|
+
import {
|
|
3
|
+
captureNetwork,
|
|
4
|
+
networkCounter,
|
|
5
|
+
networkId,
|
|
6
|
+
networkList,
|
|
7
|
+
networkRecord,
|
|
8
|
+
requireNetwork,
|
|
9
|
+
} from '../private/networkcontract.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether Cloudly holds the Corestore control token, and at which revision. Value-free.
|
|
13
|
+
*
|
|
14
|
+
* There is one token for both Corestore provider configs, so this is one state rather than one per
|
|
15
|
+
* provider. `revision` is 1 for the first token ever stored and advances by exactly one with every
|
|
16
|
+
* accepted set, whichever path set it. No request removes a token, so a present token never turns
|
|
17
|
+
* absent again.
|
|
18
|
+
*/
|
|
19
|
+
export type TCorestoreControlTokenState =
|
|
20
|
+
| { state: 'absent' }
|
|
21
|
+
| { state: 'present'; revision: number };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What an administrator names to store the Corestore control token: the token, and the state the
|
|
25
|
+
* administrator last read. Never a minimum: a set that committed after that read is refused rather
|
|
26
|
+
* than overwritten.
|
|
27
|
+
*
|
|
28
|
+
* `token` is the secret itself, the only member of this contract that carries it. A sender never
|
|
29
|
+
* logs, hooks or persists the request; Cloudly never answers or records the token.
|
|
30
|
+
*/
|
|
31
|
+
export interface ICorestoreControlTokenSetRequest {
|
|
32
|
+
token: string;
|
|
33
|
+
expected: TCorestoreControlTokenState;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Which path stored a token: an administrator's typed request, or the `secrets
|
|
38
|
+
* set-corestore-control-token` command run inside Cloudly's container.
|
|
39
|
+
*/
|
|
40
|
+
export type TCorestoreControlTokenSetPath = 'admin-request' | 'container-cli';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The audit record of one accepted set, which Cloudly writes in the transaction that stores the
|
|
44
|
+
* token. Append-only and value-free: it names which revision was stored, by whom, how and when,
|
|
45
|
+
* never the token or a hash of it.
|
|
46
|
+
*/
|
|
47
|
+
export interface ICorestoreControlTokenSetReceipt {
|
|
48
|
+
id: string;
|
|
49
|
+
/** The revision this set stored. */
|
|
50
|
+
revision: number;
|
|
51
|
+
via: TCorestoreControlTokenSetPath;
|
|
52
|
+
/** The administrator read from the verified credential, or `operator:secrets-cli`. */
|
|
53
|
+
actorId: string;
|
|
54
|
+
setAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What Cloudly answers to an accepted set: the state it produced and its receipt. Value-free. */
|
|
58
|
+
export interface ICorestoreControlTokenSetResult {
|
|
59
|
+
state: { state: 'present'; revision: number };
|
|
60
|
+
receipt: ICorestoreControlTokenSetReceipt;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The token's state and its most recent receipts, oldest first: every receipt while the token has
|
|
65
|
+
* been set at most `corestoreControlTokenMaximumReceipts` times, the latest that many after. Value-free.
|
|
66
|
+
*/
|
|
67
|
+
export interface ICorestoreControlTokenStateRead {
|
|
68
|
+
state: TCorestoreControlTokenState;
|
|
69
|
+
receipts: ICorestoreControlTokenSetReceipt[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Every reason Cloudly answers instead of storing a Corestore control token. */
|
|
73
|
+
export const corestoreControlTokenSetRefusals = Object.freeze([
|
|
74
|
+
/** The token is outside the contract: its length, encoding or whitespace (`isCorestoreControlToken`). */
|
|
75
|
+
'token-invalid',
|
|
76
|
+
/** `expected` said absent, and a token is stored. */
|
|
77
|
+
'token-present',
|
|
78
|
+
/** `expected` named a revision, and no token is stored. */
|
|
79
|
+
'token-absent',
|
|
80
|
+
/** `expected` named a revision, and the stored token is at another one. */
|
|
81
|
+
'token-revision-mismatch',
|
|
82
|
+
/** A Corestore provider config is missing or disabled, so there is nothing to store it under. */
|
|
83
|
+
'provider-unavailable',
|
|
84
|
+
] as const);
|
|
85
|
+
|
|
86
|
+
export type TCorestoreControlTokenSetRefusal = typeof corestoreControlTokenSetRefusals[number];
|
|
87
|
+
|
|
88
|
+
/** The actor a receipt names when the in-container command stored the token. */
|
|
89
|
+
export const corestoreControlTokenContainerCliActorId = 'operator:secrets-cli' as const;
|
|
90
|
+
|
|
91
|
+
/** The most receipts one state read answers: the latest ones, once a token has been set more often. */
|
|
92
|
+
export const corestoreControlTokenMaximumReceipts = 4096;
|
|
93
|
+
|
|
94
|
+
const fail = (): never => { throw new Error('Corestore control token contract rejected.'); };
|
|
95
|
+
const safe = <T>(operationArg: () => T): T => {
|
|
96
|
+
try { return operationArg(); } catch { return fail(); }
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const readState = (valueArg: unknown): TCorestoreControlTokenState => {
|
|
100
|
+
const value = networkRecord(valueArg, ['state'], ['revision']);
|
|
101
|
+
if (value.state === 'absent') {
|
|
102
|
+
networkRecord(value, ['state']);
|
|
103
|
+
return { state: 'absent' };
|
|
104
|
+
}
|
|
105
|
+
requireNetwork(value.state === 'present');
|
|
106
|
+
return { state: 'present', revision: networkCounter(networkRecord(value, ['state', 'revision']).revision) };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const snapshotCorestoreControlTokenState = (
|
|
110
|
+
valueArg: unknown,
|
|
111
|
+
): TCorestoreControlTokenState => safe(() => readState(captureNetwork(valueArg)));
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Structural copy of a set request. A refusal never quotes the request, because it carries the
|
|
115
|
+
* token: every failure is the contract's one value-free message.
|
|
116
|
+
*/
|
|
117
|
+
export const snapshotCorestoreControlTokenSetRequest = (
|
|
118
|
+
valueArg: unknown,
|
|
119
|
+
): ICorestoreControlTokenSetRequest => safe(() => {
|
|
120
|
+
const value = networkRecord(captureNetwork(valueArg), ['token', 'expected']);
|
|
121
|
+
requireNetwork(isCorestoreControlToken(value.token));
|
|
122
|
+
return { token: value.token, expected: readState(value.expected) };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const readReceipt = (valueArg: unknown): ICorestoreControlTokenSetReceipt => {
|
|
126
|
+
const value = networkRecord(valueArg, ['id', 'revision', 'via', 'actorId', 'setAt']);
|
|
127
|
+
requireNetwork(value.via === 'admin-request' || value.via === 'container-cli');
|
|
128
|
+
const actorId = networkId(value.actorId);
|
|
129
|
+
requireNetwork(value.via === 'admin-request'
|
|
130
|
+
? actorId !== corestoreControlTokenContainerCliActorId
|
|
131
|
+
: actorId === corestoreControlTokenContainerCliActorId);
|
|
132
|
+
return {
|
|
133
|
+
id: networkId(value.id),
|
|
134
|
+
revision: networkCounter(value.revision),
|
|
135
|
+
via: value.via,
|
|
136
|
+
actorId,
|
|
137
|
+
setAt: networkCounter(value.setAt),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const snapshotCorestoreControlTokenSetReceipt = (
|
|
142
|
+
valueArg: unknown,
|
|
143
|
+
): ICorestoreControlTokenSetReceipt => safe(() => readReceipt(captureNetwork(valueArg)));
|
|
144
|
+
|
|
145
|
+
export const snapshotCorestoreControlTokenSetResult = (
|
|
146
|
+
valueArg: unknown,
|
|
147
|
+
): ICorestoreControlTokenSetResult => safe(() => {
|
|
148
|
+
const value = networkRecord(captureNetwork(valueArg), ['state', 'receipt']);
|
|
149
|
+
const state = readState(value.state);
|
|
150
|
+
requireNetwork(state.state === 'present');
|
|
151
|
+
return { state, receipt: readReceipt(value.receipt) };
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Structural copy of a state read. No token stored means no receipt. A stored token's receipts are
|
|
156
|
+
* contiguous and end at its revision, and they start at revision 1 -- or at 2, because only a token
|
|
157
|
+
* stored before receipts existed reaches revision 1 without one -- unless the read is a full window
|
|
158
|
+
* of `corestoreControlTokenMaximumReceipts`, whose oldest receipt may name any later revision.
|
|
159
|
+
*/
|
|
160
|
+
export const snapshotCorestoreControlTokenStateRead = (
|
|
161
|
+
valueArg: unknown,
|
|
162
|
+
): ICorestoreControlTokenStateRead => safe(() => {
|
|
163
|
+
const value = networkRecord(captureNetwork(valueArg), ['state', 'receipts']);
|
|
164
|
+
const state = readState(value.state);
|
|
165
|
+
const receipts = networkList(value.receipts, corestoreControlTokenMaximumReceipts, readReceipt);
|
|
166
|
+
if (state.state === 'absent') {
|
|
167
|
+
requireNetwork(receipts.length === 0);
|
|
168
|
+
return { state, receipts };
|
|
169
|
+
}
|
|
170
|
+
const first = state.revision - receipts.length + 1;
|
|
171
|
+
requireNetwork(first >= 1
|
|
172
|
+
&& (first <= 2 || receipts.length === corestoreControlTokenMaximumReceipts)
|
|
173
|
+
&& receipts.every((receipt, index) => receipt.revision === first + index));
|
|
174
|
+
return { state, receipts };
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Joins a set answer to the request it answers. The answered state is the one the request
|
|
179
|
+
* produces -- revision 1 when no token was stored, the expected revision plus one otherwise -- and
|
|
180
|
+
* the receipt is an administrator's receipt for exactly that revision. Content join only: whether
|
|
181
|
+
* the token was stored is Cloudly's to prove, and the answer never carries it.
|
|
182
|
+
*/
|
|
183
|
+
export const bindCorestoreControlTokenSet = (
|
|
184
|
+
resultArg: unknown,
|
|
185
|
+
requestArg: unknown,
|
|
186
|
+
): ICorestoreControlTokenSetResult => safe(() => {
|
|
187
|
+
const request = snapshotCorestoreControlTokenSetRequest(requestArg);
|
|
188
|
+
const result = snapshotCorestoreControlTokenSetResult(resultArg);
|
|
189
|
+
const produced = request.expected.state === 'absent' ? 1 : request.expected.revision + 1;
|
|
190
|
+
requireNetwork(Number.isSafeInteger(produced)
|
|
191
|
+
&& result.state.revision === produced
|
|
192
|
+
&& result.receipt.revision === produced
|
|
193
|
+
&& result.receipt.via === 'admin-request');
|
|
194
|
+
return result;
|
|
195
|
+
});
|
package/ts/data/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './clusteringressdesignation.js';
|
|
|
7
7
|
export * from './clusterrelaycredential.js';
|
|
8
8
|
export * from './config.js';
|
|
9
9
|
export * from './corestore.js';
|
|
10
|
+
export * from './corestorecontroltoken.js';
|
|
10
11
|
export * from './corestorelegacyadoption.js';
|
|
11
12
|
export * from './coremail.js';
|
|
12
13
|
export * from './coremail.runtime.js';
|
|
@@ -38,6 +39,7 @@ export * from './networkaddressplan.js';
|
|
|
38
39
|
export * from './networkauthority.js';
|
|
39
40
|
export * from './networkcredential.js';
|
|
40
41
|
export * from './networkdelivery.js';
|
|
42
|
+
export * from './networkegressretirement.js';
|
|
41
43
|
export * from './networklease.js';
|
|
42
44
|
export * from './networknode.js';
|
|
43
45
|
export * from './networkprojection.js';
|