@serve.zone/interfaces 31.8.0 → 31.9.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.
Files changed (40) hide show
  1. package/changelog.md +45 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/data/clusterruntime.d.ts +39 -0
  4. package/dist_ts/data/clusterruntime.js +11 -0
  5. package/dist_ts/data/index.d.ts +4 -0
  6. package/dist_ts/data/index.js +5 -1
  7. package/dist_ts/data/networkaddressplan.d.ts +78 -0
  8. package/dist_ts/data/networkaddressplan.js +114 -0
  9. package/dist_ts/data/networkauthority.js +5 -17
  10. package/dist_ts/data/networknode.d.ts +37 -0
  11. package/dist_ts/data/networknode.js +6 -0
  12. package/dist_ts/data/runtimeconfig.js +14 -77
  13. package/dist_ts/data/serviceruntime.d.ts +128 -0
  14. package/dist_ts/data/serviceruntime.js +92 -0
  15. package/dist_ts/private/networkcontract.d.ts +4 -0
  16. package/dist_ts/private/networkcontract.js +15 -1
  17. package/dist_ts/private/producercontract.d.ts +11 -0
  18. package/dist_ts/private/producercontract.js +21 -0
  19. package/dist_ts/private/runtimepolicy.d.ts +31 -0
  20. package/dist_ts/private/runtimepolicy.js +90 -0
  21. package/dist_ts/requests/cluster.d.ts +37 -0
  22. package/dist_ts/requests/cluster.js +1 -1
  23. package/dist_ts/requests/network.d.ts +57 -1
  24. package/dist_ts/requests/service.d.ts +60 -0
  25. package/package.json +1 -1
  26. package/readme.md +120 -0
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/data/clusterruntime.ts +45 -0
  29. package/ts/data/index.ts +4 -0
  30. package/ts/data/networkaddressplan.ts +175 -0
  31. package/ts/data/networkauthority.ts +4 -16
  32. package/ts/data/networknode.ts +37 -0
  33. package/ts/data/runtimeconfig.ts +17 -76
  34. package/ts/data/serviceruntime.ts +208 -0
  35. package/ts/private/networkcontract.ts +14 -0
  36. package/ts/private/producercontract.ts +19 -0
  37. package/ts/private/runtimepolicy.ts +105 -0
  38. package/ts/requests/cluster.ts +45 -0
  39. package/ts/requests/network.ts +50 -1
  40. package/ts/requests/service.ts +81 -0
@@ -149,6 +149,20 @@ export const networkPrefixBounds = (value: string): [number, number] => {
149
149
  const [address, bits] = value.split('/'), first = networkIpv4Number(address);
150
150
  return [first, first + 2 ** (32 - Number(bits)) - 1];
151
151
  };
152
+ /** A canonical IPv4 prefix: a dotted quad whose host bits are zero. */
153
+ export const networkPrefix = (value: unknown): string => {
154
+ requireNetwork(typeof value === 'string' && value.length <= 18);
155
+ const parts = value.split('/');
156
+ requireNetwork(parts.length === 2 && /^(?:[0-9]|[12][0-9]|3[0-2])$/.test(parts[1]));
157
+ requireNetwork(networkIpv4Number(networkIpv4(parts[0])) % 2 ** (32 - Number(parts[1])) === 0);
158
+ return value;
159
+ };
160
+ /** A canonical prefix inside the RFC 1918 private ranges. */
161
+ export const networkPrivatePrefix = (value: unknown): string => {
162
+ const prefix = networkPrefix(value);
163
+ requireNetwork(['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'].some(range => networkContains(range, prefix)));
164
+ return prefix;
165
+ };
152
166
  export const networkContains = (outer: string, inner: string): boolean => {
153
167
  const [a, b] = networkPrefixBounds(outer), [c, d] = networkPrefixBounds(inner); return a <= c && b >= d;
154
168
  };
@@ -0,0 +1,19 @@
1
+ import { requireNetwork } from './networkcontract.js';
2
+
3
+ /**
4
+ * The runtime producer records read decoded input with the network family's strict JSON readers,
5
+ * and every producer snapshot runs inside `producerSafe`: whichever rule refused, a caller sees
6
+ * this one rejection and learns nothing about which field failed.
7
+ */
8
+ export const failProducer = (): never => { throw new Error('Runtime producer contract rejected.'); };
9
+ export const producerSafe = <T>(operation: () => T): T => { try { return operation(); } catch { return failProducer(); } };
10
+
11
+ /** Membership in a closed union whose members are listed once, as the array the union is derived from. */
12
+ export const isOneOf = <T extends string>(members: readonly T[], value: unknown): value is T =>
13
+ (members as readonly unknown[]).includes(value);
14
+
15
+ /** The tag of a captured tagged record, read before the record's exact keys are known. */
16
+ export const variantOf = (value: unknown, tag: string): unknown => {
17
+ requireNetwork(value !== null && typeof value === 'object' && !Array.isArray(value));
18
+ return (value as Record<string, unknown>)[tag];
19
+ };
@@ -0,0 +1,105 @@
1
+ import type { IRuntimeConfigLogPolicyV1 } from '../data/runtimeconfig.js';
2
+
3
+ /**
4
+ * The execution policy a runtime config seals and a service runtime profile states ahead of it.
5
+ * Both validators apply these timing, HTTP request, CPU, memory, identity and log rules. Only the
6
+ * service's target ports decide whether a readiness target exists, which a profile cannot know.
7
+ *
8
+ * Every check here reads captured plain data only, the output of `snapshotBoundedJson` or
9
+ * `captureNetwork`: on live input a getter could pass a check and then hand the caller another value.
10
+ */
11
+ export const runtimePolicyLimits = Object.freeze({
12
+ minimumMemoryBytes: 16 * 1024 * 1024,
13
+ maximumMemoryBytes: 2 ** 50,
14
+ maximumCpuMillis: 1_000_000,
15
+ maximumIdentityNumber: 2 ** 31 - 1,
16
+ maximumReadinessDurationMs: 3_600_000,
17
+ maximumLogRetentionMs: 31_536_000_000,
18
+ } as const);
19
+
20
+ export type TRuntimePolicyFailure = (messageArg: string) => never;
21
+
22
+ export const isBoundedInteger = (valueArg: unknown, minimumArg: number, maximumArg: number): valueArg is number =>
23
+ typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && !Object.is(valueArg, -0)
24
+ && valueArg >= minimumArg && valueArg <= maximumArg;
25
+ export const isRuntimePortName = (valueArg: unknown): valueArg is string =>
26
+ typeof valueArg === 'string' && /^[a-z0-9][a-z0-9_.-]{0,62}$/.test(valueArg);
27
+ export const isExactRecord = (
28
+ valueArg: unknown, keysArg: readonly string[],
29
+ ): valueArg is Record<string, unknown> => {
30
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
31
+ const keys = Object.keys(valueArg);
32
+ return keys.length === keysArg.length && keys.every((keyArg) => keysArg.includes(keyArg));
33
+ };
34
+
35
+ /** null explicitly requests no CPU quota; zero is never a quota. */
36
+ export const isRuntimeCpuMillis = (valueArg: unknown): valueArg is number | null =>
37
+ valueArg === null || isBoundedInteger(valueArg, 1, runtimePolicyLimits.maximumCpuMillis);
38
+ export const isRuntimeMemoryBytes = (valueArg: unknown): valueArg is number =>
39
+ isBoundedInteger(valueArg, runtimePolicyLimits.minimumMemoryBytes, runtimePolicyLimits.maximumMemoryBytes);
40
+ export const isRuntimeIdentityNumber = (valueArg: unknown): valueArg is number =>
41
+ isBoundedInteger(valueArg, 0, runtimePolicyLimits.maximumIdentityNumber);
42
+
43
+ export const isRuntimeLogPolicy = (valueArg: unknown): valueArg is IRuntimeConfigLogPolicyV1 =>
44
+ isExactRecord(valueArg, ['maximumLineBytes', 'maximumBufferedBytes', 'retentionMs'])
45
+ && isBoundedInteger(valueArg.maximumLineBytes, 1, 1_048_576)
46
+ && isBoundedInteger(valueArg.maximumBufferedBytes, 1, 67_108_864)
47
+ && valueArg.maximumBufferedBytes >= valueArg.maximumLineBytes
48
+ && isBoundedInteger(valueArg.retentionMs, 1, runtimePolicyLimits.maximumLogRetentionMs);
49
+
50
+ const timingKeys = ['initialDelayMs', 'startupGraceMs', 'intervalMs', 'timeoutMs',
51
+ 'successThreshold', 'failureThreshold'] as const;
52
+
53
+ /**
54
+ * Readiness policy. `selectsTcpTargetArg` decides whether a probe's named target and port exist:
55
+ * a config answers from its own target ports, a profile ahead of any config cannot know them.
56
+ */
57
+ export const assertRuntimeReadiness = (
58
+ valueArg: unknown,
59
+ selectsTcpTargetArg: (targetPortArg: string, portArg: number) => boolean,
60
+ failArg: TRuntimePolicyFailure,
61
+ ): void => {
62
+ if (!valueArg || typeof valueArg !== 'object') failArg('runtime config readiness is required');
63
+ const value = valueArg as Record<string, unknown>;
64
+ const keys = value.kind === 'process' ? ['kind', ...timingKeys]
65
+ : value.kind === 'tcp' ? ['kind', ...timingKeys, 'targetPort', 'port']
66
+ : value.kind === 'http' ? ['kind', ...timingKeys, 'targetPort', 'port',
67
+ 'scheme', 'host', 'path', 'expectedStatusCodes', 'followRedirects'] : [];
68
+ const maximumDurationMs = runtimePolicyLimits.maximumReadinessDurationMs;
69
+ if (keys.length === 0 || !isExactRecord(value, keys)
70
+ || !isBoundedInteger(value.initialDelayMs, 0, maximumDurationMs)
71
+ || !isBoundedInteger(value.startupGraceMs, 0, maximumDurationMs)
72
+ || !isBoundedInteger(value.intervalMs, 100, maximumDurationMs)
73
+ || !isBoundedInteger(value.timeoutMs, 1, 60_000)
74
+ || !isBoundedInteger(value.successThreshold, 1, 100)
75
+ || !isBoundedInteger(value.failureThreshold, 1, 100)
76
+ || value.timeoutMs > value.intervalMs || value.initialDelayMs > value.startupGraceMs) {
77
+ failArg('runtime config readiness must use explicit bounded timing and exact schema');
78
+ }
79
+ if (value.kind === 'process') return;
80
+ if (!isRuntimePortName(value.targetPort) || !isBoundedInteger(value.port, 1, 65535)
81
+ || !selectsTcpTargetArg(value.targetPort, value.port)) {
82
+ failArg('runtime config readiness must select a declared TCP target and port');
83
+ }
84
+ if (value.kind === 'tcp') return;
85
+ if ((value.scheme !== 'http' && value.scheme !== 'https') || value.followRedirects !== false
86
+ || typeof value.host !== 'string' || value.host.length > 253
87
+ || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/.test(value.host)
88
+ || typeof value.path !== 'string' || value.path.length > 4096
89
+ || !value.path.startsWith('/') || value.path.startsWith('//')
90
+ || /[^\x21-\x7e]|[\\#]/.test(value.path)
91
+ || /%(?![0-9A-F]{2})/.test(value.path)
92
+ || !Array.isArray(value.expectedStatusCodes) || value.expectedStatusCodes.length === 0
93
+ || value.expectedStatusCodes.length > 32) {
94
+ failArg('runtime config HTTP readiness must use an exact bounded request policy');
95
+ }
96
+ const parsed = new URL(value.path, 'http://runtime.invalid');
97
+ if (`${parsed.pathname}${parsed.search}` !== value.path) failArg('runtime config HTTP path must be canonical');
98
+ let previousCode = 0;
99
+ for (const code of value.expectedStatusCodes) {
100
+ if (!isBoundedInteger(code, 200, 599) || code <= previousCode) {
101
+ failArg('runtime config HTTP status expectations must be sorted unique final codes');
102
+ }
103
+ previousCode = code;
104
+ }
105
+ };
@@ -1,5 +1,7 @@
1
1
  import * as userInterfaces from '../data/user.js';
2
2
  import * as clusterInterfaces from '../data/cluster.js';
3
+ import type { IClusterRuntime, TClusterRuntimeBlocker } from '../data/clusterruntime.js';
4
+ import type { IServiceRuntimeAssignmentView } from '../data/serviceruntime.js';
3
5
  import type * as leaseInterfaces from '../data/networklease.js';
4
6
  import * as plugins from '../plugins.js';
5
7
 
@@ -182,3 +184,46 @@ export interface IReq_Any_Cloudly_DeleteClusterById extends plugins.typedrequest
182
184
  ok: boolean;
183
185
  };
184
186
  }
187
+
188
+ /**
189
+ * A cluster's runtime phase, and what still blocks it from leaving `swarm`.
190
+ *
191
+ * Cloudly refuses a cluster without a phase record as `cluster-runtime-missing` rather than
192
+ * assuming one. `blockers` is empty once nothing blocks the cutover.
193
+ */
194
+ export interface IReq_Any_Cloudly_GetClusterRuntime extends plugins.typedrequestInterfaces.implementsTR<
195
+ plugins.typedrequestInterfaces.ITypedRequest,
196
+ IReq_Any_Cloudly_GetClusterRuntime
197
+ > {
198
+ method: 'getClusterRuntime';
199
+ request: {
200
+ identity: userInterfaces.IIdentity;
201
+ clusterId: string;
202
+ };
203
+ response: {
204
+ runtime: IClusterRuntime;
205
+ blockers: TClusterRuntimeBlocker[];
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Every current runtime assignment on one cluster's nodes, a page at a time: a cluster carries
211
+ * every node's slots of every service, which no single response bounds. `limit` runs from 1 to
212
+ * `serviceRuntimeContract.maximumAssignmentPageSize`; Cloudly refuses any other.
213
+ */
214
+ export interface IReq_Any_Cloudly_GetClusterRuntimeAssignments extends plugins.typedrequestInterfaces.implementsTR<
215
+ plugins.typedrequestInterfaces.ITypedRequest,
216
+ IReq_Any_Cloudly_GetClusterRuntimeAssignments
217
+ > {
218
+ method: 'getClusterRuntimeAssignments';
219
+ request: {
220
+ identity: userInterfaces.IIdentity;
221
+ clusterId: string;
222
+ cursor: string | null;
223
+ limit: number;
224
+ };
225
+ response: {
226
+ assignments: IServiceRuntimeAssignmentView[];
227
+ nextCursor: string | null;
228
+ };
229
+ }
@@ -1,5 +1,7 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { IIdentityCredential } from '../data/user.js';
2
+ import type { IIdentity, IIdentityCredential } from '../data/user.js';
3
+ import type { IRuntimeNetworkAddressPlan, TRuntimeNetworkAddressPlanWritableData } from '../data/networkaddressplan.js';
4
+ import type { IRuntimeNetworkNodeReadiness, IRuntimeRouterIncarnation } from '../data/networknode.js';
3
5
  import type {
4
6
  IPrivateNetworkV1, IServicePrivateNetworksV1, ISetServicePrivateNetworksV1,
5
7
  TPrivateNetworkMutationV1,
@@ -98,3 +100,50 @@ export interface IRequest_Any_Cloudly_GetNetworkNodes {
98
100
  networkNodes: plugins.tsclass.network.INetworkNode[];
99
101
  };
100
102
  }
103
+
104
+ /**
105
+ * Writes the runtime network's address plan.
106
+ *
107
+ * `expectedRevision` is the revision the caller read, or `null` when no plan exists yet. Cloudly
108
+ * validates the plan with `snapshotRuntimeNetworkAddressPlan`, decides the change with
109
+ * `admitRuntimeNetworkAddressPlanChange`, refuses the change while a resolver or platform endpoint
110
+ * it removes or changes is still referenced, and answers with the stored plan: at its new revision,
111
+ * or unchanged.
112
+ */
113
+ export interface IReq_Any_Cloudly_SetRuntimeNetworkAddressPlan extends plugins.typedrequestInterfaces.implementsTR<
114
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_SetRuntimeNetworkAddressPlan
115
+ > {
116
+ method: 'setRuntimeNetworkAddressPlan';
117
+ request: { identity: IIdentity; expectedRevision: number | null; plan: TRuntimeNetworkAddressPlanWritableData };
118
+ response: { plan: IRuntimeNetworkAddressPlan };
119
+ }
120
+
121
+ /** `null` until an administrator sets the first plan; the runtime network stays unconfigured until then. */
122
+ export interface IReq_Any_Cloudly_GetRuntimeNetworkAddressPlan extends plugins.typedrequestInterfaces.implementsTR<
123
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetRuntimeNetworkAddressPlan
124
+ > {
125
+ method: 'getRuntimeNetworkAddressPlan';
126
+ request: { identity: IIdentity };
127
+ response: { plan: IRuntimeNetworkAddressPlan | null };
128
+ }
129
+
130
+ /** Whether one node's runtime network is ready for workloads, and every condition still missing. */
131
+ export interface IReq_Any_Cloudly_GetRuntimeNetworkNodeReadiness extends plugins.typedrequestInterfaces.implementsTR<
132
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetRuntimeNetworkNodeReadiness
133
+ > {
134
+ method: 'getRuntimeNetworkNodeReadiness';
135
+ request: { identity: IIdentity; nodeId: string };
136
+ response: IRuntimeNetworkNodeReadiness;
137
+ }
138
+
139
+ /**
140
+ * Advances one node's router incarnation, so the node producer reserves the router a fresh handoff.
141
+ * `expectedIncarnation` is the incarnation the caller read; any other refuses the request.
142
+ */
143
+ export interface IReq_Any_Cloudly_ReincarnateRuntimeNetworkRouter extends plugins.typedrequestInterfaces.implementsTR<
144
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_ReincarnateRuntimeNetworkRouter
145
+ > {
146
+ method: 'reincarnateRuntimeNetworkRouter';
147
+ request: { identity: IIdentity; nodeId: string; expectedIncarnation: number };
148
+ response: { incarnation: IRuntimeRouterIncarnation };
149
+ }
@@ -16,6 +16,12 @@ import type {
16
16
  IServiceDeploymentStatus,
17
17
  } from '../data/deploymentoperation.js';
18
18
  import type { TSha256Digest } from '../data/immutableimage.js';
19
+ import type {
20
+ IServiceRuntimeAssignmentView,
21
+ IServiceRuntimeSpec,
22
+ IServiceRuntimeStatus,
23
+ TServiceRuntimeSpecWritableData,
24
+ } from '../data/serviceruntime.js';
19
25
  import type {
20
26
  IServiceDeploymentAdoptionInput,
21
27
  IServiceDeploymentAdoptionImagePromotionInput,
@@ -370,3 +376,78 @@ extends plugins.typedrequestInterfaces.implementsTR<
370
376
  success: boolean;
371
377
  };
372
378
  }
379
+
380
+ /**
381
+ * Writes the complete runtime spec of one service.
382
+ *
383
+ * `expectedRevision` is the revision the caller read, or `null` when the service has never had a
384
+ * spec; any other stored revision refuses the write. Cloudly copies the organization from the
385
+ * service and answers with the stored spec at its new revision.
386
+ */
387
+ export interface IRequest_Any_Cloudly_SetServiceRuntimeSpec
388
+ extends plugins.typedrequestInterfaces.implementsTR<
389
+ plugins.typedrequestInterfaces.ITypedRequest,
390
+ IRequest_Any_Cloudly_SetServiceRuntimeSpec
391
+ > {
392
+ method: 'setServiceRuntimeSpec';
393
+ request: {
394
+ identity: IIdentity;
395
+ serviceId: string;
396
+ expectedRevision: number | null;
397
+ spec: TServiceRuntimeSpecWritableData;
398
+ };
399
+ response: {
400
+ spec: IServiceRuntimeSpec;
401
+ };
402
+ }
403
+
404
+ /**
405
+ * A service's runtime spec and the producer's verdict on it. Both are `null` before the first spec;
406
+ * a spec's status is `null` until the producer first decides it.
407
+ */
408
+ export interface IRequest_Any_Cloudly_GetServiceRuntimeSpec
409
+ extends plugins.typedrequestInterfaces.implementsTR<
410
+ plugins.typedrequestInterfaces.ITypedRequest,
411
+ IRequest_Any_Cloudly_GetServiceRuntimeSpec
412
+ > {
413
+ method: 'getServiceRuntimeSpec';
414
+ request: {
415
+ identity: IIdentity;
416
+ serviceId: string;
417
+ };
418
+ response:
419
+ | { spec: null; status: null }
420
+ | { spec: IServiceRuntimeSpec; status: IServiceRuntimeStatus | null };
421
+ }
422
+
423
+ /** Every current runtime assignment of one service, one per replica slot. */
424
+ export interface IRequest_Any_Cloudly_GetServiceRuntimeAssignments
425
+ extends plugins.typedrequestInterfaces.implementsTR<
426
+ plugins.typedrequestInterfaces.ITypedRequest,
427
+ IRequest_Any_Cloudly_GetServiceRuntimeAssignments
428
+ > {
429
+ method: 'getServiceRuntimeAssignments';
430
+ request: {
431
+ identity: IIdentity;
432
+ serviceId: string;
433
+ };
434
+ response: {
435
+ assignments: IServiceRuntimeAssignmentView[];
436
+ };
437
+ }
438
+
439
+ /**
440
+ * Cloudly -> admin UIs: a service's runtime spec, status or assignments changed. It carries no
441
+ * state, so a late or repeated push can never overwrite a newer read; the UI reads the views again.
442
+ */
443
+ export interface IReq_Cloudly_Any_PushServiceRuntimeChanged
444
+ extends plugins.typedrequestInterfaces.implementsTR<
445
+ plugins.typedrequestInterfaces.ITypedRequest,
446
+ IReq_Cloudly_Any_PushServiceRuntimeChanged
447
+ > {
448
+ method: 'pushServiceRuntimeChanged';
449
+ request: {
450
+ serviceId: string;
451
+ };
452
+ response: Record<string, never>;
453
+ }