@serve.zone/interfaces 32.62.0 → 32.64.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/readme.md CHANGED
@@ -627,6 +627,26 @@ locally, but only to a workload that selects it.
627
627
 
628
628
  `getRuntimeNetworkProjectionPacketGrants` derives directed member connectivity
629
629
  and explicit local public/platform egress independently of DNS readiness.
630
+
631
+ `getRuntimeNetworkProjectionPrivateMeshes` validates the complete projection and
632
+ returns its private meshes (`IRuntimeNetworkPrivateMesh`): one per
633
+ `(organizationId, networkId)` that some endpoint's service membership attaches,
634
+ with the lease references of every such endpoint, local or remote, sorted by
635
+ lease id. Meshes are sorted by organization and then network id; a network no
636
+ endpoint attaches has no mesh, and an endpoint whose membership attaches no
637
+ network is in none. This grouping is the one definition of who may reach whom:
638
+ the workload grants of `getRuntimeNetworkProjectionPacketGrants` are exactly the
639
+ ordered pairs of distinct members of each mesh with at least one end on the
640
+ projection's node, each pair once however many networks its ends share. A
641
+ receiver realizes a mesh from it without deriving membership again. Whether a
642
+ member is local is its lease's node scope (the projection's `nodeId`,
643
+ `runtimeNamespace` and `controller`), and which local members are attached, or
644
+ whether remote members are reachable through a tunnel, is the receiver's own
645
+ fact; the mesh grants nothing beyond the pairs above. A projection names each
646
+ network id once and only for the organization that owns it, so the key's
647
+ organization never separates two meshes of one validated projection; it is kept
648
+ so that the grouping holds without that rule.
649
+
630
650
  `composeRuntimeNetworkProjectionDnsViews` builds one union view per local
631
651
  workload source address, with every authorized FQDN and complete ready-replica A
632
652
  set. An empty array declares NODATA; absent/unauthorized names remain NXDOMAIN,
@@ -1305,6 +1325,14 @@ and never content, and a controller and a node cannot disagree about the referen
1305
1325
  absence: an assignment that states no endpoint is different content, and so a different digest, from
1306
1326
  one that does.
1307
1327
 
1328
+ `designation` is the optional role its cluster gives the attempt's service, from
1329
+ `data.runtimeAssignmentDesignations`: `cluster-ingress` for the service Cloudly lists as the cluster's
1330
+ designated ingress (and delivers first). A node begins such a run before every other run it holds,
1331
+ by this statement rather than by delivery order. It is part of the attempt, so every later revision
1332
+ states the same value, and it grants nothing. A reader built before the member refuses an assignment
1333
+ that states it, so a controller states it only on a session whose offer speaks the release that added
1334
+ it or a later one.
1335
+
1308
1336
  All fields except the digest itself participate in a domain-separated
1309
1337
  SHA-256 digest. Digests provide integrity and compare-and-swap identity;
1310
1338
  authentication must separately establish the trusted controller/node scope.
@@ -1564,6 +1592,15 @@ answer, more than a second beyond Cloudly's budget and below TypedSocket's 10 s
1564
1592
  connection-restore deadline); and a relay's `relaySessionForwardMs` (8 s) for a
1565
1593
  session read or report forwarded over its established Cloudly link.
1566
1594
 
1595
+ `data.runtimeSessionPushBounds.maximumInFlightPerNode` (8) is the other direction: how many requests a
1596
+ controller holds outstanding on one node's runtime session at once, every method and lane together
1597
+ (assignments, local storage claims, the network signing authority, projection and DNS lease renewals,
1598
+ stats reads), counted per node rather than per relay peer. A node holds that many beside the answers to
1599
+ its own requests and their cancellations without closing its session, and answers each within the
1600
+ relay's forward timeout for a push to a node (coreflow's `nodeForwardMs`, 8 s; not
1601
+ `runtimeSessionRequestBounds.relaySessionForwardMs`, which bounds the node's own requests to the
1602
+ controller); a controller that sends more may lose the session.
1603
+
1567
1604
  ### Assignment-Bound Runtime Secrets
1568
1605
 
1569
1606
  `@serve.zone/interfaces/runtime` exports the Pallet assignment-secret contract.
@@ -3511,7 +3548,9 @@ checks the hostname's source profile stays the only way in whether or not the no
3511
3548
  reachable from elsewhere. `validateClusterRouteSourceAllowList(value, path?)` judges one list. A
3512
3549
  reader built before the member refuses the whole table that carries it, so an ingress that applies
3513
3550
  it registers with `clusterIngressCapabilities.routeSourceAllowList` (`route-source-allowlist`), and
3514
- Cloudly sends the member only to such an ingress through a relay that reads it.
3551
+ Cloudly sends the member only to such an ingress through a relay that reads it. An ingress that
3552
+ reads the registration refusal `controller-deadline` registers with
3553
+ `clusterIngressCapabilities.controllerDeadlineRefusal` (`controller-deadline-refusal`).
3515
3554
  `IClusterPortRoute` forwards a TCP or UDP listen port without terminating anything. `validateClusterRouteTable(value, path?)` returns one
3516
3555
  named reason per violation, refuses two routes for one hostname and two port routes for one
3517
3556
  transport and port, and — because a validation reason travels into logs — **never quotes
@@ -3565,8 +3604,15 @@ the order Cloudly judges them: `not-ready` (Cloudly cannot judge registrations y
3565
3604
  not the connection Cloudly elected for its cluster), `ingress-not-designated`, `node-mismatch` (the
3566
3605
  node is not one the designation lets the ingress run on), `assignment-not-running` (no ready
3567
3606
  assignment of the ingress service runs on that node under its current session, or it does not carry
3568
- the designation's current credential), `bearer-refused` and `conflict` (the designation, assignment
3569
- or session changed while the registration was judged). `retryAfterMs` is how long the ingress waits
3607
+ the designation's current credential), `bearer-refused`, `conflict` (the designation, assignment
3608
+ or session changed while the registration was judged) and `controller-deadline` (Cloudly did not
3609
+ settle the registration within its request budget, `runtimeSessionRequestBounds.controllerBudgetMs`;
3610
+ a momentary state of Cloudly's, so the ingress registers again soon, and a registration whose commit
3611
+ the deadline interrupted is answered as its replay). A reader built before `controller-deadline`
3612
+ reads such a refusal as no named refusal, so Cloudly sends it only to an ingress whose registration
3613
+ states `clusterIngressCapabilities.controllerDeadlineRefusal` (`controller-deadline-refusal`) and
3614
+ answers `not-ready` to every other; the relay forwards a refusal untouched and never reads its
3615
+ reason. `retryAfterMs` is how long the ingress waits
3570
3616
  before it registers again, 1 s to 300 s (`clusterIngressRegistrationContract`).
3571
3617
  `validateClusterIngressRegistrationRefusal` names every violation and
3572
3618
  `readClusterIngressRegistrationRefusal(errorData)` answers the refusal or `null`, reading plain data
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.62.0',
6
+ version: '32.64.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -44,9 +44,15 @@ export const clusterIngressLimits = Object.freeze({
44
44
  * `routeSourceAllowList`: the ingress applies `IClusterHttpRoute.sourceAllowList`. A route table
45
45
  * that carries the member is refused whole by a reader built before it, so Cloudly sends a route
46
46
  * with a source allowlist only to an ingress that registered with this capability.
47
+ *
48
+ * `controllerDeadlineRefusal`: the ingress reads the registration refusal `controller-deadline`
49
+ * (`clusterIngressRegistrationRefusalReasons`) as a named refusal and waits its `retryAfterMs`. A
50
+ * reader built before the reason reads such a refusal as no named refusal, so Cloudly answers it
51
+ * only to a registration that states this capability and answers `not-ready` to every other.
47
52
  */
48
53
  export const clusterIngressCapabilities = Object.freeze({
49
54
  routeSourceAllowList: 'route-source-allowlist',
55
+ controllerDeadlineRefusal: 'controller-deadline-refusal',
50
56
  } as const);
51
57
 
52
58
  export type TClusterIngressCapability =
@@ -618,6 +624,14 @@ export const clusterIngressRegistrationRefusalReasons = Object.freeze([
618
624
  'bearer-refused',
619
625
  /** The designation, the assignment or the session changed while the registration was judged. */
620
626
  'conflict',
627
+ /**
628
+ * Cloudly did not settle the registration within its request budget
629
+ * (`runtimeSessionRequestBounds.controllerBudgetMs`): a momentary state of Cloudly's, never a
630
+ * verdict on the ingress, which registers again soon. A registration whose commit the deadline
631
+ * interrupted may have been accepted; registering again is answered as its replay. Sent only to
632
+ * an ingress that registered with `clusterIngressCapabilities.controllerDeadlineRefusal`.
633
+ */
634
+ 'controller-deadline',
621
635
  ] as const);
622
636
 
623
637
  export type TClusterIngressRegistrationRefusalReason = typeof clusterIngressRegistrationRefusalReasons[number];
package/ts/data/index.ts CHANGED
@@ -46,6 +46,7 @@ export * from './networklease.js';
46
46
  export * from './networknode.js';
47
47
  export * from './networkprojection.js';
48
48
  export type { IRuntimeNetworkRouterPacketGrant, IRuntimeNetworkRouterProjection } from './networkrouter.js';
49
+ export type { IRuntimeNetworkPrivateMesh } from './networkprivatemesh.js';
49
50
  export type { IRuntimeNetworkHostIngress, IRuntimeNetworkHostPacketGrant, IRuntimeNetworkHostProjection } from './networkhost.js';
50
51
  export { runtimeNetworkHostContract } from './networkhost.js';
51
52
  export type { IRuntimeNetworkWorkloadIngress, IRuntimeNetworkWorkloadIngressPacketGrant,
@@ -67,6 +68,7 @@ export * from './runtimestorageclaim.js';
67
68
  export * from './runtimelocalstorageclaim.js';
68
69
  export * from './runtimelocalvolumearchive.js';
69
70
  export * from './runtimesession.js';
71
+ export * from './runtimesessionpush.js';
70
72
  export * from './runtimesessionrefusal.js';
71
73
  export * from './runtimeterminalreceipt.js';
72
74
  export * from './secret.js';
@@ -0,0 +1,45 @@
1
+ import type { IRuntimeAuthorityReference } from './runtimeassignment.js';
2
+ import type { IServicePrivateNetworks } from './privatenetwork.js';
3
+ import type { IRuntimeNetworkEndpoint } from './networkprojection.js';
4
+ import { networkCanonical, requireNetwork } from '../private/networkcontract.js';
5
+
6
+ /**
7
+ * One private network of one organization and every projection endpoint, local or remote, whose
8
+ * service membership attaches it. Any two members on different nodes may reach each other, and no
9
+ * other workload may: this is the grouping the projection's workload packet grants are made of.
10
+ */
11
+ export interface IRuntimeNetworkPrivateMesh {
12
+ organizationId: string;
13
+ networkId: string;
14
+ /** The lease references of the members, sorted by lease id; never empty. */
15
+ members: IRuntimeAuthorityReference[];
16
+ }
17
+
18
+ const ownerKey = (value: { organizationId: string; serviceId: string }): string => networkCanonical([value.organizationId, value.serviceId]);
19
+
20
+ /**
21
+ * Internal grouping after capture. The public helper validates the whole projection. Keyed by the
22
+ * membership's organization and the attached network id, sorted by both, with members sorted by
23
+ * lease id; a network no endpoint's membership attaches has no mesh.
24
+ */
25
+ export const privateMeshEndpointsOf = (projection: {
26
+ memberships: IServicePrivateNetworks[];
27
+ endpoints: IRuntimeNetworkEndpoint[];
28
+ }): Array<{ organizationId: string; networkId: string; members: IRuntimeNetworkEndpoint[] }> => {
29
+ const memberships = new Map(projection.memberships.map(member => [ownerKey(member), member]));
30
+ const meshes = new Map<string, { organizationId: string; networkId: string; members: IRuntimeNetworkEndpoint[] }>();
31
+ for (const endpoint of projection.endpoints) {
32
+ const member = memberships.get(ownerKey(endpoint.lease)); requireNetwork(member);
33
+ for (const attachment of member.attachments) {
34
+ const key = networkCanonical([member.organizationId, attachment.networkId]);
35
+ let mesh = meshes.get(key);
36
+ if (!mesh) meshes.set(key, mesh = { organizationId: member.organizationId, networkId: attachment.networkId, members: [] });
37
+ mesh.members.push(endpoint);
38
+ }
39
+ }
40
+ const byId = (left: IRuntimeNetworkEndpoint, right: IRuntimeNetworkEndpoint) =>
41
+ left.lease.id < right.lease.id ? -1 : left.lease.id > right.lease.id ? 1 : 0;
42
+ return [...meshes.values()].sort((left, right) => left.organizationId < right.organizationId ? -1
43
+ : left.organizationId > right.organizationId ? 1 : left.networkId < right.networkId ? -1 : left.networkId > right.networkId ? 1 : 0)
44
+ .map(mesh => ({ ...mesh, members: mesh.members.sort(byId) }));
45
+ };
@@ -9,6 +9,7 @@ import { routerPacketGrantsOf, snapshotRouterProjection,
9
9
  type IRuntimeNetworkRouterPacketGrant, type IRuntimeNetworkRouterProjection } from './networkrouter.js';
10
10
  import { hostPacketGrantsOf, snapshotHostProjection,
11
11
  type IRuntimeNetworkHostPacketGrant, type IRuntimeNetworkHostProjection } from './networkhost.js';
12
+ import { privateMeshEndpointsOf, type IRuntimeNetworkPrivateMesh } from './networkprivatemesh.js';
12
13
  import { snapshotWorkloadIngressProjection, workloadIngressPacketGrantsOf,
13
14
  type IRuntimeNetworkWorkloadIngressPacketGrant, type IRuntimeNetworkWorkloadIngressProjection } from './networkworkloadingress.js';
14
15
  import { requireFirstProjectionRetirement, requireProjectionRetirement, snapshotRuntimeNetworkProjectionRetirement,
@@ -601,23 +602,28 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
601
602
  retiredLeases: sortedReferences(item.retiredLeases), retiredHandoffs: sortedReferences(item.retiredHandoffs), digest: networkDigest(item.digest) };
602
603
  });
603
604
 
605
+ /**
606
+ * Workload grants are the ordered pairs of distinct members of each private mesh with at least one
607
+ * end on this projection's node, each pair once however many networks its ends share; egress grants
608
+ * follow per local source. At most `maximumGrants` in all.
609
+ */
604
610
  const grantsOf = (projection: IRuntimeNetworkProjection): IRuntimeNetworkPacketGrant[] => {
605
611
  const result: IRuntimeNetworkPacketGrant[] = [];
606
- const memberships = new Map(projection.memberships.map(member => [ownerKey(member), member]));
607
612
  const append = (grant: IRuntimeNetworkPacketGrant) => {
608
613
  requireNetwork(result.length < 4096); result.push(grant);
609
614
  };
615
+ const local = new Set(projection.endpoints.filter(endpoint => networkSameScope(endpoint.lease, projection)));
616
+ const granted = new Map<IRuntimeNetworkEndpoint, Set<IRuntimeNetworkEndpoint>>();
617
+ for (const mesh of privateMeshEndpointsOf(projection)) for (const source of mesh.members) for (const destination of mesh.members) {
618
+ if (source === destination || (!local.has(source) && !local.has(destination))) continue;
619
+ let reached = granted.get(source);
620
+ if (!reached) granted.set(source, reached = new Set());
621
+ if (reached.has(destination)) continue;
622
+ reached.add(destination);
623
+ append({ source: networkReferenceOf(source.lease), destination: { kind: 'workload', lease: networkReferenceOf(destination.lease) } });
624
+ }
610
625
  for (const source of projection.endpoints) {
611
- const member = memberships.get(ownerKey(source.lease)); requireNetwork(member);
612
- for (const destination of projection.endpoints) {
613
- if (source === destination || (!networkSameScope(source.lease, projection) && !networkSameScope(destination.lease, projection))) continue;
614
- const target = memberships.get(ownerKey(destination.lease)); requireNetwork(target);
615
- if (member.organizationId === target.organizationId && member.attachments.some(attachment =>
616
- target.attachments.some(other => other.networkId === attachment.networkId))) {
617
- append({ source: networkReferenceOf(source.lease), destination: { kind: 'workload', lease: networkReferenceOf(destination.lease) } });
618
- }
619
- }
620
- if (!networkSameScope(source.lease, projection)) continue;
626
+ if (!local.has(source)) continue;
621
627
  if (source.publicEgress) append({ source: networkReferenceOf(source.lease), destination: { kind: 'public' } });
622
628
  for (const id of source.platformEndpointIds) {
623
629
  const platform = projection.protectedAuthority.platformEndpoints.find(item => item.id === id); requireNetwork(platform);
@@ -726,6 +732,16 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
726
732
  export const getRuntimeNetworkProjectionPacketGrants = async (value: unknown): Promise<IRuntimeNetworkPacketGrant[]> =>
727
733
  grantsOf(await validateRuntimeNetworkProjection(value));
728
734
 
735
+ /**
736
+ * Every private network of every organization with the projection endpoints, local or remote, whose
737
+ * membership attaches it (`IRuntimeNetworkPrivateMesh`): the grouping the workload packet grants
738
+ * are made of, sorted by organization and network id. Which members a node realizes is its own
739
+ * decision; who may reach whom is this one.
740
+ */
741
+ export const getRuntimeNetworkProjectionPrivateMeshes = async (value: unknown): Promise<IRuntimeNetworkPrivateMesh[]> =>
742
+ privateMeshEndpointsOf(await validateRuntimeNetworkProjection(value)).map(mesh => ({ organizationId: mesh.organizationId,
743
+ networkId: mesh.networkId, members: mesh.members.map(member => networkReferenceOf(member.lease)) }));
744
+
729
745
  /** Router-origin authority comes only from explicit signed selections, never DNS names or workload grants. */
730
746
  export const getRuntimeNetworkProjectionRouterPacketGrants = async (value: unknown): Promise<IRuntimeNetworkRouterPacketGrant[]> =>
731
747
  routerPacketGrantsOf(await validateRuntimeNetworkProjection(value));
@@ -39,6 +39,19 @@ export type TRuntimeAssignmentDisposition =
39
39
  | 'stop-preserve'
40
40
  | 'remove-runtime-preserve-storage';
41
41
 
42
+ /**
43
+ * What a cluster designates the service of an attempt as, stated by the controller so a node can
44
+ * order its work by the service's role instead of by the order the deliveries arrived in.
45
+ *
46
+ * `cluster-ingress`: the service is its cluster's designated ingress, the service Cloudly lists as
47
+ * designated (`IClusterIngressDesignation` for a cluster born on Pallet,
48
+ * `IFleetCutoverIngressDesignation` after a Swarm cutover) and delivers first, because a cluster
49
+ * serves nothing until its ingress runs. A node begins such a run before every other run it holds.
50
+ */
51
+ export const runtimeAssignmentDesignations = ['cluster-ingress'] as const;
52
+
53
+ export type TRuntimeAssignmentDesignation = (typeof runtimeAssignmentDesignations)[number];
54
+
42
55
  export interface IRuntimeAssignment extends IRuntimeAssignmentScope {
43
56
  assignmentId: string;
44
57
  serviceId: string;
@@ -55,6 +68,19 @@ export interface IRuntimeAssignment extends IRuntimeAssignmentScope {
55
68
  disposition: TRuntimeAssignmentDisposition;
56
69
  /** No automatic relocation or expiry-based claim of a stopped writer. */
57
70
  offlinePolicy: 'continue-node-bound';
71
+ /**
72
+ * The cluster's designation of this attempt's service (`runtimeAssignmentDesignations`); absent
73
+ * for a service the cluster designates nothing. It is part of the attempt, as every member but
74
+ * `generation`, `previous`, `disposition` and `digest` is: every later revision of the attempt
75
+ * states the same value, so a designation that moves to another service reaches the nodes with
76
+ * the services' next attempts. It orders a node's work and grants nothing: no authority,
77
+ * placement, credential or network access follows from it.
78
+ *
79
+ * A reader built before this member refuses an assignment that states it (exact keys), so a
80
+ * controller states it only on a session whose offer speaks the release that added it or a later
81
+ * one, and states nothing on any other.
82
+ */
83
+ designation?: TRuntimeAssignmentDesignation;
58
84
  workload: {
59
85
  /**
60
86
  * The registry that published this image, which is the publication identity the image release
@@ -320,15 +346,21 @@ const assignmentKeys = [
320
346
  'assignmentId', 'serviceId', 'replicaId', 'attemptId', 'workerGroupId', 'siteId',
321
347
  'generation', 'previous', 'disposition', 'offlinePolicy', 'workload', 'digest',
322
348
  ] as const;
349
+ /** Stated only for a service its cluster designates (`runtimeAssignmentDesignations`). */
350
+ const optionalAssignmentKeys = ['designation'] as const;
323
351
 
324
352
  const assertAssignment = (valueArg: unknown): IRuntimeAssignment => {
325
- if (!isExactRecord(valueArg, assignmentKeys)
353
+ if (!isExactRecord(valueArg, assignmentKeys, optionalAssignmentKeys)
326
354
  || !isController(valueArg.controller)
327
355
  || !isPositiveInteger(valueArg.generation) || !isDigest(valueArg.digest)
328
356
  || valueArg.offlinePolicy !== 'continue-node-bound'
329
357
  || !['run', 'stop-preserve', 'remove-runtime-preserve-storage'].includes(
330
358
  valueArg.disposition as string,
331
359
  )) fail('runtime assignment must use its exact schema');
360
+ if (Object.hasOwn(valueArg, 'designation')
361
+ && !runtimeAssignmentDesignations.some((designationArg) => designationArg === valueArg.designation)) {
362
+ fail('runtime assignment designation must be a stated designation');
363
+ }
332
364
  for (const key of ['organizationId', 'runtimeNamespace', 'assignmentId', 'serviceId', 'attemptId']) {
333
365
  if (!isIdentifier(valueArg[key])) fail(`runtime assignment ${key} must be a canonical identifier`);
334
366
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * How many requests a controller holds outstanding on one node's runtime session at once.
3
+ *
4
+ * Counted per node, never per lane, method or carrier: every request a controller sends a node over
5
+ * its runtime session counts — `applyRuntimeAssignment`, `applyRuntimeLocalStorageClaim`,
6
+ * `applyRuntimeNetworkSigningAuthority`, `applyRuntimeNetworkProjection`,
7
+ * `applyRuntimeNetworkDnsLeaseRenewal` and `readRuntimeAssignmentStats` together, whatever pass or
8
+ * lane of the controller sends it — from the moment it is sent until its answer or its failure
9
+ * arrived. One relay peer carries a whole cluster, so the count is each node's own, not the peer's.
10
+ * A relay adds no request of its own while the controller's session is up (its DNS lease escrow
11
+ * offers one renewal per node, and only while that session is down), so it stays within the same
12
+ * count.
13
+ *
14
+ * A node holds this many at once beside the answers to its own requests and the cancellation each
15
+ * of them may draw, and never closes its session over them; it answers each within the relay's
16
+ * forward timeout for a push to a node (coreflow's `nodeForwardMs`, 8 s; not
17
+ * `runtimeSessionRequestBounds.relaySessionForwardMs`, which bounds the node's own requests to the
18
+ * controller), so a request the controller stopped waiting for is no longer held on the node. A
19
+ * controller that sends more than this may lose the node's session: the node's transport bounds
20
+ * what it queues and closes the connection past that bound.
21
+ */
22
+ export const runtimeSessionPushBounds = Object.freeze({
23
+ maximumInFlightPerNode: 8,
24
+ } as const);