@serve.zone/interfaces 31.8.0 → 32.0.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 (215) hide show
  1. package/.smartconfig.json +1 -0
  2. package/changelog.md +512 -0
  3. package/dist_ts/00_commitinfo_data.js +3 -3
  4. package/dist_ts/appstore/types.d.ts +4 -6
  5. package/dist_ts/appstore/types.js +5 -5
  6. package/dist_ts/data/backup.d.ts +3 -26
  7. package/dist_ts/data/cluster.d.ts +36 -11
  8. package/dist_ts/data/cluster.js +33 -22
  9. package/dist_ts/data/clusteringress.d.ts +8 -0
  10. package/dist_ts/data/clusteringress.js +13 -8
  11. package/dist_ts/data/clusternode.d.ts +121 -74
  12. package/dist_ts/data/clusternode.js +109 -132
  13. package/dist_ts/data/clusterruntime.d.ts +39 -0
  14. package/dist_ts/data/clusterruntime.js +11 -0
  15. package/dist_ts/data/coremail.d.ts +3 -7
  16. package/dist_ts/data/coremail.js +2 -2
  17. package/dist_ts/data/coremail.runtime.d.ts +0 -1
  18. package/dist_ts/data/coremail.runtime.js +5 -22
  19. package/dist_ts/data/deploymentadoption.d.ts +0 -2
  20. package/dist_ts/data/deploymentadoption.js +40 -6
  21. package/dist_ts/data/deploymentoperation.js +16 -3
  22. package/dist_ts/data/deploymentpreflight.d.ts +0 -2
  23. package/dist_ts/data/deploymentpreflight.js +1 -1
  24. package/dist_ts/data/immutableimage.d.ts +38 -21
  25. package/dist_ts/data/immutableimage.js +45 -5
  26. package/dist_ts/data/index.d.ts +5 -1
  27. package/dist_ts/data/index.js +5 -1
  28. package/dist_ts/data/isolatedrestore.d.ts +139 -21
  29. package/dist_ts/data/isolatedrestore.golden.d.ts +0 -1
  30. package/dist_ts/data/isolatedrestore.golden.js +6 -9
  31. package/dist_ts/data/isolatedrestore.js +176 -40
  32. package/dist_ts/data/mail.d.ts +1 -1
  33. package/dist_ts/data/networkaddressplan.d.ts +78 -0
  34. package/dist_ts/data/networkaddressplan.js +114 -0
  35. package/dist_ts/data/networkauthority.d.ts +20 -22
  36. package/dist_ts/data/networkauthority.js +29 -43
  37. package/dist_ts/data/networkcredential.d.ts +10 -10
  38. package/dist_ts/data/networkcredential.js +16 -16
  39. package/dist_ts/data/networkdelivery.d.ts +18 -18
  40. package/dist_ts/data/networkdelivery.js +16 -16
  41. package/dist_ts/data/networklease.d.ts +18 -7
  42. package/dist_ts/data/networklease.js +29 -7
  43. package/dist_ts/data/networknode.d.ts +37 -0
  44. package/dist_ts/data/networknode.js +6 -0
  45. package/dist_ts/data/networkprojection.d.ts +59 -60
  46. package/dist_ts/data/networkprojection.js +53 -57
  47. package/dist_ts/data/networkprotection.d.ts +27 -28
  48. package/dist_ts/data/networkprotection.js +36 -36
  49. package/dist_ts/data/networkrouter.d.ts +11 -11
  50. package/dist_ts/data/networkrouter.js +1 -1
  51. package/dist_ts/data/networksigning.d.ts +16 -18
  52. package/dist_ts/data/networksigning.js +20 -21
  53. package/dist_ts/data/nodecredential.d.ts +4 -6
  54. package/dist_ts/data/nodeenrollment.d.ts +35 -40
  55. package/dist_ts/data/nodeenrollment.js +57 -72
  56. package/dist_ts/data/privatenetwork.d.ts +21 -24
  57. package/dist_ts/data/privatenetwork.js +39 -54
  58. package/dist_ts/data/runtimeassignment.d.ts +50 -34
  59. package/dist_ts/data/runtimeassignment.js +79 -45
  60. package/dist_ts/data/runtimeconfig.d.ts +21 -22
  61. package/dist_ts/data/runtimeconfig.js +37 -100
  62. package/dist_ts/data/runtimesession.d.ts +100 -62
  63. package/dist_ts/data/runtimesession.js +98 -79
  64. package/dist_ts/data/runtimeterminalreceipt.d.ts +28 -29
  65. package/dist_ts/data/runtimeterminalreceipt.js +42 -42
  66. package/dist_ts/data/secret.d.ts +18 -25
  67. package/dist_ts/data/secret.js +12 -52
  68. package/dist_ts/data/servicecredential.d.ts +14 -16
  69. package/dist_ts/data/servicecredential.js +17 -28
  70. package/dist_ts/data/serviceruntime.d.ts +128 -0
  71. package/dist_ts/data/serviceruntime.js +92 -0
  72. package/dist_ts/data/sparknodereport.d.ts +27 -14
  73. package/dist_ts/data/sparknodereport.js +35 -28
  74. package/dist_ts/data/webpush.d.ts +0 -1
  75. package/dist_ts/index.d.ts +2 -1
  76. package/dist_ts/index.js +3 -2
  77. package/dist_ts/platform/objectstorageretention.d.ts +21 -24
  78. package/dist_ts/platform/objectstorageretention.js +6 -18
  79. package/dist_ts/platform/storage.d.ts +1 -3
  80. package/dist_ts/platform/storagemigration.d.ts +0 -7
  81. package/dist_ts/platform/storagemigration.golden.d.ts +0 -1
  82. package/dist_ts/platform/storagemigration.golden.js +8 -12
  83. package/dist_ts/platform/storagemigration.js +6 -51
  84. package/dist_ts/platform/types.d.ts +2 -2
  85. package/dist_ts/private/canonicaljson.d.ts +9 -1
  86. package/dist_ts/private/canonicaljson.js +14 -2
  87. package/dist_ts/private/dataproperties.d.ts +62 -0
  88. package/dist_ts/private/dataproperties.js +117 -0
  89. package/dist_ts/private/networkcontract.d.ts +14 -10
  90. package/dist_ts/private/networkcontract.js +15 -1
  91. package/dist_ts/private/producercontract.d.ts +11 -0
  92. package/dist_ts/private/producercontract.js +21 -0
  93. package/dist_ts/private/releaseversion.d.ts +37 -0
  94. package/dist_ts/private/releaseversion.js +48 -0
  95. package/dist_ts/private/runtimepolicy.d.ts +31 -0
  96. package/dist_ts/private/runtimepolicy.js +90 -0
  97. package/dist_ts/private/tokenlist.d.ts +9 -0
  98. package/dist_ts/private/tokenlist.js +23 -0
  99. package/dist_ts/protocol.d.ts +137 -0
  100. package/dist_ts/protocol.js +255 -0
  101. package/dist_ts/requests/admin.d.ts +8 -8
  102. package/dist_ts/requests/backup.d.ts +102 -112
  103. package/dist_ts/requests/cluster.d.ts +47 -10
  104. package/dist_ts/requests/cluster.js +1 -1
  105. package/dist_ts/requests/clusteringress.d.ts +9 -0
  106. package/dist_ts/requests/clusteringress.js +1 -1
  107. package/dist_ts/requests/config.d.ts +3 -3
  108. package/dist_ts/requests/corestore.d.ts +0 -3
  109. package/dist_ts/requests/corestore.js +5 -9
  110. package/dist_ts/requests/network.d.ts +87 -31
  111. package/dist_ts/requests/node.d.ts +15 -28
  112. package/dist_ts/requests/pallet.d.ts +13 -13
  113. package/dist_ts/requests/runtimesession.d.ts +53 -41
  114. package/dist_ts/requests/secret.d.ts +4 -5
  115. package/dist_ts/requests/secret.js +1 -5
  116. package/dist_ts/requests/service.d.ts +60 -0
  117. package/dist_ts/runtime.corestore.d.ts +56 -74
  118. package/dist_ts/runtime.corestore.databaseexportpackage.d.ts +30 -36
  119. package/dist_ts/runtime.corestore.databaseexportpackage.golden.d.ts +11 -12
  120. package/dist_ts/runtime.corestore.databaseexportpackage.golden.js +54 -71
  121. package/dist_ts/runtime.corestore.databaseexportpackage.js +9 -25
  122. package/dist_ts/runtime.corestore.js +35 -135
  123. package/dist_ts/runtime.corestore.relay.js +2 -7
  124. package/dist_ts/runtime.d.ts +38 -48
  125. package/dist_ts/runtime.js +20 -77
  126. package/dist_ts/runtime.networkprojection.d.ts +4 -4
  127. package/dist_ts/runtime.networkprojection.js +18 -18
  128. package/dist_ts/runtime.workloadinit.d.ts +23 -24
  129. package/dist_ts/runtime.workloadinit.js +6 -13
  130. package/package.json +2 -2
  131. package/readme.md +540 -269
  132. package/ts/00_commitinfo_data.ts +3 -3
  133. package/ts/appstore/types.ts +4 -6
  134. package/ts/data/backup.ts +3 -38
  135. package/ts/data/cluster.ts +51 -34
  136. package/ts/data/clusteringress.ts +12 -12
  137. package/ts/data/clusternode.ts +204 -232
  138. package/ts/data/clusterruntime.ts +45 -0
  139. package/ts/data/coremail.runtime.ts +4 -21
  140. package/ts/data/coremail.ts +3 -7
  141. package/ts/data/deploymentadoption.ts +46 -4
  142. package/ts/data/deploymentoperation.ts +16 -1
  143. package/ts/data/deploymentpreflight.ts +0 -2
  144. package/ts/data/immutableimage.ts +71 -24
  145. package/ts/data/index.ts +5 -1
  146. package/ts/data/isolatedrestore.golden.ts +5 -9
  147. package/ts/data/isolatedrestore.ts +339 -54
  148. package/ts/data/mail.ts +1 -1
  149. package/ts/data/networkaddressplan.ts +175 -0
  150. package/ts/data/networkauthority.ts +45 -61
  151. package/ts/data/networkcredential.ts +24 -24
  152. package/ts/data/networkdelivery.ts +27 -27
  153. package/ts/data/networklease.ts +40 -13
  154. package/ts/data/networknode.ts +37 -0
  155. package/ts/data/networkprojection.ts +119 -124
  156. package/ts/data/networkprotection.ts +54 -55
  157. package/ts/data/networkrouter.ts +14 -14
  158. package/ts/data/networksigning.ts +29 -32
  159. package/ts/data/nodecredential.ts +4 -6
  160. package/ts/data/nodeenrollment.ts +96 -114
  161. package/ts/data/privatenetwork.ts +58 -74
  162. package/ts/data/runtimeassignment.ts +143 -94
  163. package/ts/data/runtimeconfig.ts +62 -122
  164. package/ts/data/runtimesession.ts +185 -138
  165. package/ts/data/runtimeterminalreceipt.ts +72 -73
  166. package/ts/data/secret.ts +28 -76
  167. package/ts/data/servicecredential.ts +33 -43
  168. package/ts/data/serviceruntime.ts +208 -0
  169. package/ts/data/sparknodereport.ts +48 -36
  170. package/ts/data/webpush.ts +0 -1
  171. package/ts/index.ts +2 -0
  172. package/ts/platform/objectstorageretention.ts +36 -55
  173. package/ts/platform/storage.ts +1 -3
  174. package/ts/platform/storagemigration.golden.ts +7 -11
  175. package/ts/platform/storagemigration.ts +5 -55
  176. package/ts/platform/types.ts +2 -2
  177. package/ts/private/canonicaljson.ts +13 -1
  178. package/ts/private/dataproperties.ts +139 -0
  179. package/ts/private/networkcontract.ts +23 -9
  180. package/ts/private/producercontract.ts +19 -0
  181. package/ts/private/releaseversion.ts +70 -0
  182. package/ts/private/runtimepolicy.ts +105 -0
  183. package/ts/private/tokenlist.ts +30 -0
  184. package/ts/protocol.ts +364 -0
  185. package/ts/requests/admin.ts +11 -11
  186. package/ts/requests/backup.ts +112 -117
  187. package/ts/requests/cluster.ts +58 -13
  188. package/ts/requests/clusteringress.ts +9 -0
  189. package/ts/requests/config.ts +3 -3
  190. package/ts/requests/corestore.ts +4 -16
  191. package/ts/requests/network.ts +90 -41
  192. package/ts/requests/node.ts +21 -38
  193. package/ts/requests/pallet.ts +17 -17
  194. package/ts/requests/runtimesession.ts +63 -51
  195. package/ts/requests/secret.ts +5 -10
  196. package/ts/requests/service.ts +81 -0
  197. package/ts/runtime.corestore.databaseexportpackage.golden.ts +77 -95
  198. package/ts/runtime.corestore.databaseexportpackage.ts +51 -73
  199. package/ts/runtime.corestore.relay.ts +1 -10
  200. package/ts/runtime.corestore.ts +118 -268
  201. package/ts/runtime.networkprojection.ts +30 -30
  202. package/ts/runtime.ts +83 -158
  203. package/ts/runtime.workloadinit.ts +36 -48
  204. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.d.ts +0 -902
  205. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.d.ts +0 -35
  206. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.js +0 -315
  207. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.js +0 -3315
  208. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.d.ts +0 -291
  209. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.d.ts +0 -30
  210. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.js +0 -408
  211. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.js +0 -1126
  212. package/ts/runtime.cloudlylegacydeploymentsettlement.golden.ts +0 -421
  213. package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.ts +0 -510
  214. package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.ts +0 -2470
  215. package/ts/runtime.cloudlylegacydeploymentsettlement.ts +0 -6630
@@ -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,70 @@
1
+ /**
2
+ * The release-version grammar every serve.zone package agrees on.
3
+ *
4
+ * Exactly what a published release is named and nothing else: no prerelease, because gitzone
5
+ * refuses to publish one, and no build metadata, because two strings that name one release would
6
+ * then compare unequal. Components are read as numbers, which is the whole reason this rule
7
+ * exists — `32.10.0` is above `32.9.0`, and a lexical comparison of the strings says the opposite.
8
+ */
9
+ export interface IReleaseVersion {
10
+ major: number;
11
+ minor: number;
12
+ patch: number;
13
+ }
14
+
15
+ /** Canonical decimal components: no leading zeros, no prefix, no suffix. */
16
+ const releaseVersionPattern = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
17
+
18
+ /**
19
+ * Bounds the string before anything parses it.
20
+ *
21
+ * Every component of a real release fits many times over, so the only values this refuses are
22
+ * digit runs that no release could carry and that a reader would otherwise have to count.
23
+ */
24
+ export const maximumReleaseVersionLength = 32;
25
+
26
+ /**
27
+ * The three components of one release version, or `null` for everything that is not one.
28
+ *
29
+ * A component beyond the safe-integer range is refused rather than rounded: two such versions
30
+ * would compare equal after rounding, and an ordering that cannot tell two releases apart is
31
+ * worse than no answer.
32
+ */
33
+ export const parseReleaseVersion = (valueArg: unknown): IReleaseVersion | null => {
34
+ if (typeof valueArg !== 'string' || valueArg.length > maximumReleaseVersionLength) {
35
+ return null;
36
+ }
37
+ const components = releaseVersionPattern.exec(valueArg);
38
+ if (components === null) {
39
+ return null;
40
+ }
41
+ const [major, minor, patch] = components.slice(1).map(Number);
42
+ if (!Number.isSafeInteger(major) || !Number.isSafeInteger(minor) || !Number.isSafeInteger(patch)) {
43
+ return null;
44
+ }
45
+ return { major, minor, patch };
46
+ };
47
+
48
+ /**
49
+ * Whether a value names one published release.
50
+ *
51
+ * Every rule of the grammar goes through `parseReleaseVersion`, including the numeric one, so a
52
+ * validator that accepts a version cannot accept one the comparison then refuses to order.
53
+ */
54
+ export const isReleaseVersion = (valueArg: unknown): valueArg is string => (
55
+ parseReleaseVersion(valueArg) !== null
56
+ );
57
+
58
+ /** Negative, zero or positive as the left version is below, equal to or above the right one. */
59
+ export const compareReleaseVersions = (
60
+ leftArg: IReleaseVersion,
61
+ rightArg: IReleaseVersion,
62
+ ): number => {
63
+ if (leftArg.major !== rightArg.major) {
64
+ return leftArg.major - rightArg.major;
65
+ }
66
+ if (leftArg.minor !== rightArg.minor) {
67
+ return leftArg.minor - rightArg.minor;
68
+ }
69
+ return leftArg.patch - rightArg.patch;
70
+ };
@@ -0,0 +1,105 @@
1
+ import type { IRuntimeConfigLogPolicy } 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 IRuntimeConfigLogPolicy =>
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
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Judging a list of tokens a peer sent against the vocabulary it may draw from.
3
+ *
4
+ * Every such list in this package is compared byte for byte by its receiver, so they all obey one
5
+ * rule; a second spelling of that rule elsewhere would be a second answer to what a canonical list
6
+ * is. A caller adds whatever else its own contract demands — a required subset, a bound — as its
7
+ * own clause beside this one.
8
+ */
9
+ import { compareWireTokens } from './canonicaljson.js';
10
+
11
+ /**
12
+ * True when a value is exactly a canonical token list: known names, strictly increasing.
13
+ *
14
+ * Strictly increasing states sorted and duplicate-free in one rule, which is what lets a replayed
15
+ * list be compared byte for byte instead of as a set. A hole is refused rather than skipped over:
16
+ * array iteration passes holes by, so a sparse list would satisfy every rule about its entries
17
+ * while carrying none of the entries a receiver then compares.
18
+ */
19
+ export const isCanonicalTokenList = <TTokenGeneric extends string>(
20
+ valueArg: unknown,
21
+ vocabularyArg: readonly TTokenGeneric[],
22
+ ): valueArg is TTokenGeneric[] => (
23
+ Array.isArray(valueArg)
24
+ && Object.keys(valueArg).length === valueArg.length
25
+ && valueArg.every((entryArg, indexArg) => (
26
+ vocabularyArg.some((tokenArg) => tokenArg === entryArg)
27
+ && (indexArg === 0
28
+ || compareWireTokens(valueArg[indexArg - 1] as string, entryArg as string) < 0)
29
+ ))
30
+ );
package/ts/protocol.ts ADDED
@@ -0,0 +1,364 @@
1
+ import { commitinfo } from './00_commitinfo_data.js';
2
+ import {
3
+ isPlainRecord, readDataProperty, readExactDataRecord,
4
+ } from './private/dataproperties.js';
5
+ import {
6
+ compareReleaseVersions,
7
+ isReleaseVersion,
8
+ maximumReleaseVersionLength,
9
+ parseReleaseVersion,
10
+ type IReleaseVersion,
11
+ } from './private/releaseversion.js';
12
+
13
+ /**
14
+ * The contract this build speaks: the installed `@serve.zone/interfaces` release.
15
+ *
16
+ * There is no protocol number beside it and no per-shape version anywhere in this package. One
17
+ * release of these interfaces is exactly one contract, so naming the release is the complete
18
+ * statement about what a peer sends and what it can read.
19
+ */
20
+ export const protocolVersion: string = commitinfo.version;
21
+
22
+ /**
23
+ * What one side of a session speaks, and the oldest peer it accepts.
24
+ *
25
+ * Frozen across majors together with `IProtocolRefusal`: a peer built against the next major must
26
+ * still be able to read this, or the mismatch it exists to name arrives as an unreadable body and
27
+ * degrades into a generic contract rejection.
28
+ */
29
+ export interface IProtocolOffer {
30
+ /** `MAJOR.MINOR.PATCH` of the `@serve.zone/interfaces` release the sender was built against. */
31
+ interfacesVersion: string;
32
+ /**
33
+ * The oldest peer release this side accepts: in the same major as `interfacesVersion` and never
34
+ * above it. A deployable raises it in the commit that starts depending on a later minor, for the
35
+ * session kind that depends on it, which is what orders a rollout: peers first.
36
+ */
37
+ minimumPeerVersion: string;
38
+ }
39
+
40
+ const protocolOfferKeys = ['interfacesVersion', 'minimumPeerVersion'] as const;
41
+
42
+ /**
43
+ * Why two offers cannot serve one session, judged from the side that refuses.
44
+ *
45
+ * `peer-below-minimum` and `self-below-peer-minimum` are the same fact seen from the two sides,
46
+ * which is why both exist: a log line names who was too old without the reader having to know
47
+ * which side wrote it.
48
+ */
49
+ const protocolIncompatibilities = [
50
+ 'major-mismatch',
51
+ 'peer-below-minimum',
52
+ 'self-below-peer-minimum',
53
+ ] as const;
54
+
55
+ export type TProtocolIncompatibility = (typeof protocolIncompatibilities)[number];
56
+
57
+ /**
58
+ * The named refusal, carried as `TypedResponseError.errorData` on a TypedRequest hop and as the
59
+ * response body on the Spark heartbeat's refusal status. Frozen across majors.
60
+ */
61
+ export interface IProtocolRefusal {
62
+ code: 'protocol-incompatible';
63
+ /** Judged from the refusing side. */
64
+ reason: TProtocolIncompatibility;
65
+ /** The offer of the side that refused. */
66
+ refusing: IProtocolOffer;
67
+ /** The offer it refused. */
68
+ refused: IProtocolOffer;
69
+ }
70
+
71
+ const protocolRefusalKeys = ['code', 'reason', 'refusing', 'refused'] as const;
72
+
73
+ export type TProtocolNegotiation =
74
+ | { compatible: true }
75
+ | { compatible: false; refusal: IProtocolRefusal };
76
+
77
+ /**
78
+ * The cadence at which a refused client offers again.
79
+ *
80
+ * Shared so that no client invents its own: a refusal stands until one of the two sides is
81
+ * upgraded, which is an operator action, so offering faster only costs connections.
82
+ */
83
+ export const refusedOfferRetryIntervalMs = 300_000;
84
+
85
+ /**
86
+ * The status a route that carries no typed error answers a refusal with, body `IProtocolRefusal`.
87
+ *
88
+ * The Spark routes are plain HTTPS POSTs, so the refusal needs a status of its own: 422 says the
89
+ * body was read and judged, which is exactly what happened, and separates a refused offer from the
90
+ * 401 an unknown node gets and the 400 a malformed body gets.
91
+ */
92
+ export const protocolRefusalHttpStatus = 422;
93
+
94
+ const exactSchemaMessage = (pathArg: string): string => `${pathArg} must use its exact schema`;
95
+
96
+ const releaseVersionMessage = (pathArg: string): string => (
97
+ `${pathArg} must be a MAJOR.MINOR.PATCH release version of at most `
98
+ + `${maximumReleaseVersionLength} characters, without prerelease or build metadata`
99
+ );
100
+
101
+ const requireReleaseVersion = (valueArg: unknown, pathArg: string): IReleaseVersion => {
102
+ const version = parseReleaseVersion(valueArg);
103
+ if (version === null) {
104
+ throw new Error(releaseVersionMessage(pathArg));
105
+ }
106
+ return version;
107
+ };
108
+
109
+ /**
110
+ * The one place an offer is built: frozen, over two values that were read once and then judged.
111
+ *
112
+ * Every offer this module hands out comes from here, so what a caller holds shares nothing with
113
+ * the body it arrived on and cannot change under a session that was already negotiated.
114
+ */
115
+ const buildProtocolOffer = (
116
+ interfacesVersionArg: string,
117
+ minimumPeerVersionArg: string,
118
+ ): IProtocolOffer => Object.freeze({
119
+ interfacesVersion: interfacesVersionArg,
120
+ minimumPeerVersion: minimumPeerVersionArg,
121
+ });
122
+
123
+ /** Every reason two values are not one offer, in the order a reader fixes them. */
124
+ const offerErrors = (
125
+ interfacesVersionArg: unknown,
126
+ minimumPeerVersionArg: unknown,
127
+ pathArg: string,
128
+ ): string[] => {
129
+ const spoken = parseReleaseVersion(interfacesVersionArg);
130
+ const minimum = parseReleaseVersion(minimumPeerVersionArg);
131
+ if (spoken === null || minimum === null) {
132
+ return [
133
+ ...(spoken === null ? [releaseVersionMessage(`${pathArg} interfacesVersion`)] : []),
134
+ ...(minimum === null ? [releaseVersionMessage(`${pathArg} minimumPeerVersion`)] : []),
135
+ ];
136
+ }
137
+ return [
138
+ ...(spoken.major !== minimum.major
139
+ ? [`${pathArg} minimumPeerVersion must be in the same major as interfacesVersion`]
140
+ : []),
141
+ ...(compareReleaseVersions(minimum, spoken) > 0
142
+ ? [`${pathArg} minimumPeerVersion must not be above interfacesVersion`]
143
+ : []),
144
+ ];
145
+ };
146
+
147
+ /** What reading one value produced: the offer it carried, or every reason it carried none. */
148
+ interface IProtocolOfferRead {
149
+ offer: IProtocolOffer | null;
150
+ errors: string[];
151
+ }
152
+
153
+ /**
154
+ * Reads both fields once and judges those two values.
155
+ *
156
+ * The copy is built from what was judged rather than read again from the source: an offer arrives
157
+ * on a body a peer wrote, and the session must be served by the offer the validator saw. A field
158
+ * that is not plain data is refused by `readExactDataRecord` instead of run, so reading an offer
159
+ * never executes a peer's code and cannot observe two different values.
160
+ */
161
+ const readOffer = (valueArg: unknown, pathArg = 'protocol offer'): IProtocolOfferRead => {
162
+ const offer = readExactDataRecord(valueArg, protocolOfferKeys);
163
+ if (offer === null) {
164
+ return { offer: null, errors: [exactSchemaMessage(pathArg)] };
165
+ }
166
+ const interfacesVersion: unknown = offer.interfacesVersion;
167
+ const minimumPeerVersion: unknown = offer.minimumPeerVersion;
168
+ // The grammar is judged first because the rules below compare two releases, and a value that
169
+ // names none cannot be compared to one.
170
+ if (!isReleaseVersion(interfacesVersion) || !isReleaseVersion(minimumPeerVersion)) {
171
+ return { offer: null, errors: offerErrors(interfacesVersion, minimumPeerVersion, pathArg) };
172
+ }
173
+ const errors = offerErrors(interfacesVersion, minimumPeerVersion, pathArg);
174
+ return errors.length > 0
175
+ ? { offer: null, errors }
176
+ : { offer: buildProtocolOffer(interfacesVersion, minimumPeerVersion), errors };
177
+ };
178
+
179
+ /**
180
+ * Validates one protocol offer and names every reason it is refused.
181
+ *
182
+ * Shape and self-consistency only, like every other validator in this package: whether the peer
183
+ * that sent it may open a session is the server's decision against its own records, and whether
184
+ * the two sides can serve one is `negotiateProtocol`.
185
+ */
186
+ export const validateProtocolOffer = (
187
+ valueArg: unknown,
188
+ pathArg = 'protocol offer',
189
+ ): string[] => readOffer(valueArg, pathArg).errors;
190
+
191
+ /**
192
+ * Every reason the `protocol` key of a body does not carry one offer.
193
+ *
194
+ * The naming twin of `readProtocolOffer`, for a validator that lists reasons instead of throwing:
195
+ * both read that key once as a data property, so a carrier never runs an accessor a peer put on a
196
+ * body, and both judge the copy that read produced rather than the peer's object.
197
+ */
198
+ export const validateProtocolOfferOn = (
199
+ bodyArg: unknown,
200
+ pathArg = 'protocol offer',
201
+ ): string[] => validateProtocolOffer(readDataProperty(bodyArg, 'protocol')?.value, pathArg);
202
+
203
+ /** One valid offer, detached and inert, or the first reason the value is not one. */
204
+ const requireProtocolOffer = (valueArg: unknown, pathArg: string): IProtocolOffer => {
205
+ const read = readOffer(valueArg, pathArg);
206
+ if (read.offer === null) {
207
+ throw new Error(read.errors[0]);
208
+ }
209
+ return read.offer;
210
+ };
211
+
212
+ /**
213
+ * This build's offer towards one kind of peer.
214
+ *
215
+ * The spoken version is never stated by the caller: it is the installed release, so an offer
216
+ * cannot claim a contract this build does not carry. A minimum above that release, or in another
217
+ * major, is refused here — at start-up of the deployable that declared it, rather than at its
218
+ * first registration in production.
219
+ */
220
+ export const createProtocolOffer = (minimumPeerVersionArg: string): IProtocolOffer => (
221
+ requireProtocolOffer({
222
+ interfacesVersion: protocolVersion,
223
+ minimumPeerVersion: minimumPeerVersionArg,
224
+ }, 'protocol offer')
225
+ );
226
+
227
+ /**
228
+ * The offer on a request or response body, read before anything else on that body.
229
+ *
230
+ * This is where the one server order on every carrier is written down, so that each carrier can
231
+ * refer to it instead of restating it: read the raw body as a bounded record, `readProtocolOffer`
232
+ * on it, `negotiateProtocol` against this server's offer for that session kind, answer a refusal,
233
+ * and only then the carrier's exact-key validator, the bearer and any mutation. The offer comes
234
+ * before the validator because every validator in this package is exact-key: a peer of a later
235
+ * major sends keys this build cannot validate, so a server that shape-checked first would answer
236
+ * an incompatible peer with a generic malformed-body rejection instead of the named refusal it
237
+ * backs off from. A carrier whose validator also checks `protocol` checks it for self-consistency
238
+ * within one major; the negotiation has already happened by then.
239
+ *
240
+ * Reads `protocol` alone and touches no other key, for that same reason. The returned offer is
241
+ * detached, so validating the rest of the body afterwards cannot change what was negotiated.
242
+ */
243
+ export const readProtocolOffer = (bodyArg: unknown): IProtocolOffer => {
244
+ if (!isPlainRecord(bodyArg)) {
245
+ throw new Error('protocol offer must be carried as the protocol key of a body record');
246
+ }
247
+ // One data property, read once: a `protocol` key that is an accessor carries no offer this
248
+ // module will serve a session from, exactly like a key that is missing or malformed.
249
+ return requireProtocolOffer(readDataProperty(bodyArg, 'protocol')?.value, 'protocol offer');
250
+ };
251
+
252
+ /**
253
+ * The one place an `IProtocolRefusal` is built: frozen, over two offers this module already
254
+ * detached, so a refusal states what was judged and nothing a caller can change afterwards.
255
+ */
256
+ const buildProtocolRefusal = (
257
+ reasonArg: TProtocolIncompatibility,
258
+ refusingArg: IProtocolOffer,
259
+ refusedArg: IProtocolOffer,
260
+ ): IProtocolRefusal => Object.freeze({
261
+ code: 'protocol-incompatible',
262
+ reason: reasonArg,
263
+ refusing: refusingArg,
264
+ refused: refusedArg,
265
+ });
266
+
267
+ /** One side of a negotiation: the offer it named, detached, and the two versions that offer states. */
268
+ interface INegotiatingSide {
269
+ offer: IProtocolOffer;
270
+ spoken: IReleaseVersion;
271
+ minimum: IReleaseVersion;
272
+ }
273
+
274
+ /**
275
+ * One side, read exactly once.
276
+ *
277
+ * Only the grammar is judged here: whether two offers can serve one session is the decision this
278
+ * feeds, and a side whose own two versions disagree still has to get a named answer rather than a
279
+ * different error. Reading once is what lets the refusal state what was judged while each caller
280
+ * goes on holding, and changing, its own offer object.
281
+ */
282
+ const readNegotiatingSide = (offerArg: IProtocolOffer, pathArg: string): INegotiatingSide => {
283
+ const { interfacesVersion, minimumPeerVersion } = offerArg;
284
+ const spoken = requireReleaseVersion(interfacesVersion, `${pathArg} interfacesVersion`);
285
+ const minimum = requireReleaseVersion(minimumPeerVersion, `${pathArg} minimumPeerVersion`);
286
+ return { offer: buildProtocolOffer(interfacesVersion, minimumPeerVersion), spoken, minimum };
287
+ };
288
+
289
+ /**
290
+ * Whether the two sides can serve one session, judged from the local side.
291
+ *
292
+ * Pure, and the order of the three checks is part of the contract: a major mismatch is reported as
293
+ * a major mismatch even when a minimum would also fail, because that is the fact an operator acts
294
+ * on. The two minimum rules are mirror images — one of them holding means the other holds on the
295
+ * peer, and they can never hold at once, since that would need each version to be below the other.
296
+ * An offer whose versions are not release versions is refused by throwing, at the caller that
297
+ * passed it, rather than named as an incompatibility between two peers.
298
+ */
299
+ export const negotiateProtocol = (
300
+ localArg: IProtocolOffer,
301
+ remoteArg: IProtocolOffer,
302
+ ): TProtocolNegotiation => {
303
+ const local = readNegotiatingSide(localArg, 'local protocol offer');
304
+ const remote = readNegotiatingSide(remoteArg, 'remote protocol offer');
305
+ const refuse = (reasonArg: TProtocolIncompatibility): TProtocolNegotiation => ({
306
+ compatible: false,
307
+ refusal: buildProtocolRefusal(reasonArg, local.offer, remote.offer),
308
+ });
309
+ if (local.spoken.major !== remote.spoken.major) {
310
+ return refuse('major-mismatch');
311
+ }
312
+ if (compareReleaseVersions(remote.spoken, local.minimum) < 0) {
313
+ return refuse('peer-below-minimum');
314
+ }
315
+ if (compareReleaseVersions(local.spoken, remote.minimum) < 0) {
316
+ return refuse('self-below-peer-minimum');
317
+ }
318
+ return { compatible: true };
319
+ };
320
+
321
+ const isProtocolIncompatibility = (valueArg: unknown): valueArg is TProtocolIncompatibility => (
322
+ protocolIncompatibilities.some((reasonArg) => reasonArg === valueArg)
323
+ );
324
+
325
+ /**
326
+ * The refusal inside an error payload or a response body, or `null` when the value is not one.
327
+ *
328
+ * Every transport hands it over as plain JSON, so this reads a value that survived serialization
329
+ * and answers `null` for everything else: that is how a client tells a protocol refusal, which it
330
+ * must not retry quickly, apart from every other error a server may return.
331
+ */
332
+ export const readProtocolRefusal = (valueArg: unknown): IProtocolRefusal | null => {
333
+ const refusal = readExactDataRecord(valueArg, protocolRefusalKeys);
334
+ if (refusal === null) {
335
+ return null;
336
+ }
337
+ // Every field is read once and both offers are judged as copies, so what a client waits on is
338
+ // what its own check answered `null` or a refusal for.
339
+ const code: unknown = refusal.code;
340
+ const reason: unknown = refusal.reason;
341
+ if (code !== 'protocol-incompatible' || !isProtocolIncompatibility(reason)) {
342
+ return null;
343
+ }
344
+ const refusing = readOffer(refusal.refusing).offer;
345
+ const refused = readOffer(refusal.refused).offer;
346
+ if (refusing === null || refused === null) {
347
+ return null;
348
+ }
349
+ return buildProtocolRefusal(reason, refusing, refused);
350
+ };
351
+
352
+ /**
353
+ * One line naming both sides, for the log a refusal is judged from.
354
+ *
355
+ * It opens with the code, which is the text prefix frozen across majors, so a refusal stays
356
+ * recognisable in a log written by a build nobody runs any more.
357
+ */
358
+ export const describeProtocolRefusal = (refusalArg: IProtocolRefusal): string => (
359
+ `${refusalArg.code} (${refusalArg.reason}): `
360
+ + `refusing side speaks ${refusalArg.refusing.interfacesVersion} `
361
+ + `and accepts from ${refusalArg.refusing.minimumPeerVersion}; `
362
+ + `refused side speaks ${refusalArg.refused.interfacesVersion} `
363
+ + `and accepts from ${refusalArg.refused.minimumPeerVersion}`
364
+ );
@@ -1,16 +1,16 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import type { IIdentity, IIdentityCredential, IServiceDeploymentGrant } from '../data/user.js';
3
3
  import type {
4
- IServiceMachineCredentialMetadataV1, IServiceMachineCredentialMutationResultV1,
5
- TServiceMachineCredentialMutationV1,
4
+ IServiceMachineCredentialMetadata, IServiceMachineCredentialMutationResult,
5
+ TServiceMachineCredentialMutation,
6
6
  } from '../data/servicecredential.js';
7
7
 
8
- export interface IReq_Admin_GetServiceMachineCredentialV1 extends plugins.typedrequestInterfaces.implementsTR<
9
- plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_GetServiceMachineCredentialV1
8
+ export interface IReq_Admin_GetServiceMachineCredential extends plugins.typedrequestInterfaces.implementsTR<
9
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_GetServiceMachineCredential
10
10
  > {
11
- method: 'getServiceMachineCredentialV1';
11
+ method: 'getServiceMachineCredential';
12
12
  request: { identity: IIdentityCredential; organizationId: string; serviceId: string };
13
- response: { credential: IServiceMachineCredentialMetadataV1 | null };
13
+ response: { credential: IServiceMachineCredentialMetadata | null };
14
14
  }
15
15
 
16
16
  /**
@@ -18,12 +18,12 @@ export interface IReq_Admin_GetServiceMachineCredentialV1 extends plugins.typedr
18
18
  * encrypted SecretSet delivery and the replay receipt in one fenced transaction.
19
19
  * No bearer is returned. Existing legacy deployment/cluster credentials are separate.
20
20
  */
21
- export interface IReq_Admin_MutateServiceMachineCredentialV1 extends plugins.typedrequestInterfaces.implementsTR<
22
- plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_MutateServiceMachineCredentialV1
21
+ export interface IReq_Admin_MutateServiceMachineCredential extends plugins.typedrequestInterfaces.implementsTR<
22
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_MutateServiceMachineCredential
23
23
  > {
24
- method: 'mutateServiceMachineCredentialV1';
25
- request: { identity: IIdentityCredential; mutation: TServiceMachineCredentialMutationV1 };
26
- response: IServiceMachineCredentialMutationResultV1;
24
+ method: 'mutateServiceMachineCredential';
25
+ request: { identity: IIdentityCredential; mutation: TServiceMachineCredentialMutation };
26
+ response: IServiceMachineCredentialMutationResult;
27
27
  }
28
28
 
29
29
  /** Backend-owned human credential metadata; never includes a password verifier. */