@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.
- package/.smartconfig.json +1 -0
- package/changelog.md +512 -0
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/appstore/types.d.ts +4 -6
- package/dist_ts/appstore/types.js +5 -5
- package/dist_ts/data/backup.d.ts +3 -26
- package/dist_ts/data/cluster.d.ts +36 -11
- package/dist_ts/data/cluster.js +33 -22
- package/dist_ts/data/clusteringress.d.ts +8 -0
- package/dist_ts/data/clusteringress.js +13 -8
- package/dist_ts/data/clusternode.d.ts +121 -74
- package/dist_ts/data/clusternode.js +109 -132
- package/dist_ts/data/clusterruntime.d.ts +39 -0
- package/dist_ts/data/clusterruntime.js +11 -0
- package/dist_ts/data/coremail.d.ts +3 -7
- package/dist_ts/data/coremail.js +2 -2
- package/dist_ts/data/coremail.runtime.d.ts +0 -1
- package/dist_ts/data/coremail.runtime.js +5 -22
- package/dist_ts/data/deploymentadoption.d.ts +0 -2
- package/dist_ts/data/deploymentadoption.js +40 -6
- package/dist_ts/data/deploymentoperation.js +16 -3
- package/dist_ts/data/deploymentpreflight.d.ts +0 -2
- package/dist_ts/data/deploymentpreflight.js +1 -1
- package/dist_ts/data/immutableimage.d.ts +38 -21
- package/dist_ts/data/immutableimage.js +45 -5
- package/dist_ts/data/index.d.ts +5 -1
- package/dist_ts/data/index.js +5 -1
- package/dist_ts/data/isolatedrestore.d.ts +139 -21
- package/dist_ts/data/isolatedrestore.golden.d.ts +0 -1
- package/dist_ts/data/isolatedrestore.golden.js +6 -9
- package/dist_ts/data/isolatedrestore.js +176 -40
- package/dist_ts/data/mail.d.ts +1 -1
- package/dist_ts/data/networkaddressplan.d.ts +78 -0
- package/dist_ts/data/networkaddressplan.js +114 -0
- package/dist_ts/data/networkauthority.d.ts +20 -22
- package/dist_ts/data/networkauthority.js +29 -43
- package/dist_ts/data/networkcredential.d.ts +10 -10
- package/dist_ts/data/networkcredential.js +16 -16
- package/dist_ts/data/networkdelivery.d.ts +18 -18
- package/dist_ts/data/networkdelivery.js +16 -16
- package/dist_ts/data/networklease.d.ts +18 -7
- package/dist_ts/data/networklease.js +29 -7
- package/dist_ts/data/networknode.d.ts +37 -0
- package/dist_ts/data/networknode.js +6 -0
- package/dist_ts/data/networkprojection.d.ts +59 -60
- package/dist_ts/data/networkprojection.js +53 -57
- package/dist_ts/data/networkprotection.d.ts +27 -28
- package/dist_ts/data/networkprotection.js +36 -36
- package/dist_ts/data/networkrouter.d.ts +11 -11
- package/dist_ts/data/networkrouter.js +1 -1
- package/dist_ts/data/networksigning.d.ts +16 -18
- package/dist_ts/data/networksigning.js +20 -21
- package/dist_ts/data/nodecredential.d.ts +4 -6
- package/dist_ts/data/nodeenrollment.d.ts +35 -40
- package/dist_ts/data/nodeenrollment.js +57 -72
- package/dist_ts/data/privatenetwork.d.ts +21 -24
- package/dist_ts/data/privatenetwork.js +39 -54
- package/dist_ts/data/runtimeassignment.d.ts +50 -34
- package/dist_ts/data/runtimeassignment.js +79 -45
- package/dist_ts/data/runtimeconfig.d.ts +21 -22
- package/dist_ts/data/runtimeconfig.js +37 -100
- package/dist_ts/data/runtimesession.d.ts +100 -62
- package/dist_ts/data/runtimesession.js +98 -79
- package/dist_ts/data/runtimeterminalreceipt.d.ts +28 -29
- package/dist_ts/data/runtimeterminalreceipt.js +42 -42
- package/dist_ts/data/secret.d.ts +18 -25
- package/dist_ts/data/secret.js +12 -52
- package/dist_ts/data/servicecredential.d.ts +14 -16
- package/dist_ts/data/servicecredential.js +17 -28
- package/dist_ts/data/serviceruntime.d.ts +128 -0
- package/dist_ts/data/serviceruntime.js +92 -0
- package/dist_ts/data/sparknodereport.d.ts +27 -14
- package/dist_ts/data/sparknodereport.js +35 -28
- package/dist_ts/data/webpush.d.ts +0 -1
- package/dist_ts/index.d.ts +2 -1
- package/dist_ts/index.js +3 -2
- package/dist_ts/platform/objectstorageretention.d.ts +21 -24
- package/dist_ts/platform/objectstorageretention.js +6 -18
- package/dist_ts/platform/storage.d.ts +1 -3
- package/dist_ts/platform/storagemigration.d.ts +0 -7
- package/dist_ts/platform/storagemigration.golden.d.ts +0 -1
- package/dist_ts/platform/storagemigration.golden.js +8 -12
- package/dist_ts/platform/storagemigration.js +6 -51
- package/dist_ts/platform/types.d.ts +2 -2
- package/dist_ts/private/canonicaljson.d.ts +9 -1
- package/dist_ts/private/canonicaljson.js +14 -2
- package/dist_ts/private/dataproperties.d.ts +62 -0
- package/dist_ts/private/dataproperties.js +117 -0
- package/dist_ts/private/networkcontract.d.ts +14 -10
- package/dist_ts/private/networkcontract.js +15 -1
- package/dist_ts/private/producercontract.d.ts +11 -0
- package/dist_ts/private/producercontract.js +21 -0
- package/dist_ts/private/releaseversion.d.ts +37 -0
- package/dist_ts/private/releaseversion.js +48 -0
- package/dist_ts/private/runtimepolicy.d.ts +31 -0
- package/dist_ts/private/runtimepolicy.js +90 -0
- package/dist_ts/private/tokenlist.d.ts +9 -0
- package/dist_ts/private/tokenlist.js +23 -0
- package/dist_ts/protocol.d.ts +137 -0
- package/dist_ts/protocol.js +255 -0
- package/dist_ts/requests/admin.d.ts +8 -8
- package/dist_ts/requests/backup.d.ts +102 -112
- package/dist_ts/requests/cluster.d.ts +47 -10
- package/dist_ts/requests/cluster.js +1 -1
- package/dist_ts/requests/clusteringress.d.ts +9 -0
- package/dist_ts/requests/clusteringress.js +1 -1
- package/dist_ts/requests/config.d.ts +3 -3
- package/dist_ts/requests/corestore.d.ts +0 -3
- package/dist_ts/requests/corestore.js +5 -9
- package/dist_ts/requests/network.d.ts +87 -31
- package/dist_ts/requests/node.d.ts +15 -28
- package/dist_ts/requests/pallet.d.ts +13 -13
- package/dist_ts/requests/runtimesession.d.ts +53 -41
- package/dist_ts/requests/secret.d.ts +4 -5
- package/dist_ts/requests/secret.js +1 -5
- package/dist_ts/requests/service.d.ts +60 -0
- package/dist_ts/runtime.corestore.d.ts +56 -74
- package/dist_ts/runtime.corestore.databaseexportpackage.d.ts +30 -36
- package/dist_ts/runtime.corestore.databaseexportpackage.golden.d.ts +11 -12
- package/dist_ts/runtime.corestore.databaseexportpackage.golden.js +54 -71
- package/dist_ts/runtime.corestore.databaseexportpackage.js +9 -25
- package/dist_ts/runtime.corestore.js +35 -135
- package/dist_ts/runtime.corestore.relay.js +2 -7
- package/dist_ts/runtime.d.ts +38 -48
- package/dist_ts/runtime.js +20 -77
- package/dist_ts/runtime.networkprojection.d.ts +4 -4
- package/dist_ts/runtime.networkprojection.js +18 -18
- package/dist_ts/runtime.workloadinit.d.ts +23 -24
- package/dist_ts/runtime.workloadinit.js +6 -13
- package/package.json +2 -2
- package/readme.md +540 -269
- package/ts/00_commitinfo_data.ts +3 -3
- package/ts/appstore/types.ts +4 -6
- package/ts/data/backup.ts +3 -38
- package/ts/data/cluster.ts +51 -34
- package/ts/data/clusteringress.ts +12 -12
- package/ts/data/clusternode.ts +204 -232
- package/ts/data/clusterruntime.ts +45 -0
- package/ts/data/coremail.runtime.ts +4 -21
- package/ts/data/coremail.ts +3 -7
- package/ts/data/deploymentadoption.ts +46 -4
- package/ts/data/deploymentoperation.ts +16 -1
- package/ts/data/deploymentpreflight.ts +0 -2
- package/ts/data/immutableimage.ts +71 -24
- package/ts/data/index.ts +5 -1
- package/ts/data/isolatedrestore.golden.ts +5 -9
- package/ts/data/isolatedrestore.ts +339 -54
- package/ts/data/mail.ts +1 -1
- package/ts/data/networkaddressplan.ts +175 -0
- package/ts/data/networkauthority.ts +45 -61
- package/ts/data/networkcredential.ts +24 -24
- package/ts/data/networkdelivery.ts +27 -27
- package/ts/data/networklease.ts +40 -13
- package/ts/data/networknode.ts +37 -0
- package/ts/data/networkprojection.ts +119 -124
- package/ts/data/networkprotection.ts +54 -55
- package/ts/data/networkrouter.ts +14 -14
- package/ts/data/networksigning.ts +29 -32
- package/ts/data/nodecredential.ts +4 -6
- package/ts/data/nodeenrollment.ts +96 -114
- package/ts/data/privatenetwork.ts +58 -74
- package/ts/data/runtimeassignment.ts +143 -94
- package/ts/data/runtimeconfig.ts +62 -122
- package/ts/data/runtimesession.ts +185 -138
- package/ts/data/runtimeterminalreceipt.ts +72 -73
- package/ts/data/secret.ts +28 -76
- package/ts/data/servicecredential.ts +33 -43
- package/ts/data/serviceruntime.ts +208 -0
- package/ts/data/sparknodereport.ts +48 -36
- package/ts/data/webpush.ts +0 -1
- package/ts/index.ts +2 -0
- package/ts/platform/objectstorageretention.ts +36 -55
- package/ts/platform/storage.ts +1 -3
- package/ts/platform/storagemigration.golden.ts +7 -11
- package/ts/platform/storagemigration.ts +5 -55
- package/ts/platform/types.ts +2 -2
- package/ts/private/canonicaljson.ts +13 -1
- package/ts/private/dataproperties.ts +139 -0
- package/ts/private/networkcontract.ts +23 -9
- package/ts/private/producercontract.ts +19 -0
- package/ts/private/releaseversion.ts +70 -0
- package/ts/private/runtimepolicy.ts +105 -0
- package/ts/private/tokenlist.ts +30 -0
- package/ts/protocol.ts +364 -0
- package/ts/requests/admin.ts +11 -11
- package/ts/requests/backup.ts +112 -117
- package/ts/requests/cluster.ts +58 -13
- package/ts/requests/clusteringress.ts +9 -0
- package/ts/requests/config.ts +3 -3
- package/ts/requests/corestore.ts +4 -16
- package/ts/requests/network.ts +90 -41
- package/ts/requests/node.ts +21 -38
- package/ts/requests/pallet.ts +17 -17
- package/ts/requests/runtimesession.ts +63 -51
- package/ts/requests/secret.ts +5 -10
- package/ts/requests/service.ts +81 -0
- package/ts/runtime.corestore.databaseexportpackage.golden.ts +77 -95
- package/ts/runtime.corestore.databaseexportpackage.ts +51 -73
- package/ts/runtime.corestore.relay.ts +1 -10
- package/ts/runtime.corestore.ts +118 -268
- package/ts/runtime.networkprojection.ts +30 -30
- package/ts/runtime.ts +83 -158
- package/ts/runtime.workloadinit.ts +36 -48
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.d.ts +0 -902
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.d.ts +0 -35
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.js +0 -315
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.js +0 -3315
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.d.ts +0 -291
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.d.ts +0 -30
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.js +0 -408
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.js +0 -1126
- package/ts/runtime.cloudlylegacydeploymentsettlement.golden.ts +0 -421
- package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.ts +0 -510
- package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.ts +0 -2470
- 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
|
+
);
|
package/ts/requests/admin.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
4
|
+
IServiceMachineCredentialMetadata, IServiceMachineCredentialMutationResult,
|
|
5
|
+
TServiceMachineCredentialMutation,
|
|
6
6
|
} from '../data/servicecredential.js';
|
|
7
7
|
|
|
8
|
-
export interface
|
|
9
|
-
plugins.typedrequestInterfaces.ITypedRequest,
|
|
8
|
+
export interface IReq_Admin_GetServiceMachineCredential extends plugins.typedrequestInterfaces.implementsTR<
|
|
9
|
+
plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_GetServiceMachineCredential
|
|
10
10
|
> {
|
|
11
|
-
method: '
|
|
11
|
+
method: 'getServiceMachineCredential';
|
|
12
12
|
request: { identity: IIdentityCredential; organizationId: string; serviceId: string };
|
|
13
|
-
response: { credential:
|
|
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
|
|
22
|
-
plugins.typedrequestInterfaces.ITypedRequest,
|
|
21
|
+
export interface IReq_Admin_MutateServiceMachineCredential extends plugins.typedrequestInterfaces.implementsTR<
|
|
22
|
+
plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_MutateServiceMachineCredential
|
|
23
23
|
> {
|
|
24
|
-
method: '
|
|
25
|
-
request: { identity: IIdentityCredential; mutation:
|
|
26
|
-
response:
|
|
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. */
|