@serve.zone/interfaces 32.60.0 → 32.62.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 +4 -2
- package/changelog.md +32 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/coremail.d.ts +37 -1
- package/dist_ts/data/coremail.js +23 -1
- package/dist_ts/data/coremail.runtime.d.ts +10 -1
- package/dist_ts/data/coremail.runtime.js +23 -1
- package/dist_ts/data/delegatedadmintoken.d.ts +142 -0
- package/dist_ts/data/delegatedadmintoken.js +197 -0
- package/dist_ts/data/gateway.d.ts +23 -4
- package/dist_ts/data/gateway.js +1 -1
- package/dist_ts/data/index.d.ts +2 -0
- package/dist_ts/data/index.js +3 -1
- package/dist_ts/data/mail.d.ts +36 -0
- package/dist_ts/data/mail.js +45 -2
- package/dist_ts/data/runtimeassignment.d.ts +4 -1
- package/dist_ts/data/runtimeassignment.js +5 -2
- package/dist_ts/data/secret.d.ts +23 -0
- package/dist_ts/data/secret.js +33 -1
- package/dist_ts/data/serviceruntime.d.ts +14 -1
- package/dist_ts/data/serviceruntime.js +2 -2
- package/dist_ts/data/settings.d.ts +21 -0
- package/dist_ts/data/settings.js +38 -1
- package/dist_ts/data/workloadreplicaidentity.d.ts +101 -0
- package/dist_ts/data/workloadreplicaidentity.js +159 -0
- package/dist_ts/requests/admin.d.ts +52 -0
- package/dist_ts/requests/coremail.d.ts +6 -1
- package/dist_ts/requests/identity.d.ts +4 -0
- package/package.json +5 -8
- package/readme.md +154 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/coremail.runtime.ts +26 -1
- package/ts/data/coremail.ts +42 -1
- package/ts/data/delegatedadmintoken.ts +282 -0
- package/ts/data/gateway.ts +24 -3
- package/ts/data/index.ts +2 -0
- package/ts/data/mail.ts +48 -0
- package/ts/data/runtimeassignment.ts +4 -1
- package/ts/data/secret.ts +38 -0
- package/ts/data/serviceruntime.ts +14 -1
- package/ts/data/settings.ts +46 -0
- package/ts/data/workloadreplicaidentity.ts +219 -0
- package/ts/requests/admin.ts +51 -0
- package/ts/requests/coremail.ts +6 -1
- package/ts/requests/identity.ts +4 -0
|
@@ -48,6 +48,10 @@ import {
|
|
|
48
48
|
type TCoreMailWorkloadOperation,
|
|
49
49
|
} from './coremail.js';
|
|
50
50
|
import type { IMailConnectionInfo } from './mail.js';
|
|
51
|
+
import {
|
|
52
|
+
validateWorkloadReplicaIdentity,
|
|
53
|
+
type IWorkloadReplicaIdentity,
|
|
54
|
+
} from './workloadreplicaidentity.js';
|
|
51
55
|
|
|
52
56
|
export const coreMailCanonicalJsonRules = strictCanonicalJsonRules;
|
|
53
57
|
|
|
@@ -1889,11 +1893,32 @@ export const normalizeCoreMailDesiredState = (
|
|
|
1889
1893
|
return deepFreezeValue(normalized);
|
|
1890
1894
|
};
|
|
1891
1895
|
|
|
1896
|
+
/**
|
|
1897
|
+
* The replica a CoreMail reports, read exactly as one of two closed shapes.
|
|
1898
|
+
*
|
|
1899
|
+
* CoreMail 34 and later report `IWorkloadReplicaIdentity`, the identity its runtime injects, and
|
|
1900
|
+
* it is read by that contract's own rules (`validateWorkloadReplicaIdentity`). A CoreMail before
|
|
1901
|
+
* 34 reports the deprecated `ICoreMailReplicaIdentity`, recognised by its `taskId`, and it is read
|
|
1902
|
+
* as before. Neither shape admits a member of the other.
|
|
1903
|
+
*/
|
|
1892
1904
|
export const normalizeCoreMailReplicaIdentity = (
|
|
1893
1905
|
valueArg: unknown,
|
|
1894
1906
|
pathArg = 'replica',
|
|
1895
|
-
): ICoreMailReplicaIdentity => {
|
|
1907
|
+
): IWorkloadReplicaIdentity | ICoreMailReplicaIdentity => {
|
|
1896
1908
|
const record = readRecord(valueArg, pathArg);
|
|
1909
|
+
if (!Object.hasOwn(record, 'taskId')) {
|
|
1910
|
+
assertKeys(record, pathArg, ['serviceId', 'replicaId', 'attemptId', 'imageDigest']);
|
|
1911
|
+
const errors = validateWorkloadReplicaIdentity(record);
|
|
1912
|
+
if (errors.length > 0) {
|
|
1913
|
+
fail(`${pathArg}.${errors[0]}`);
|
|
1914
|
+
}
|
|
1915
|
+
return deepFreezeValue({
|
|
1916
|
+
serviceId: record.serviceId as string,
|
|
1917
|
+
replicaId: record.replicaId as string,
|
|
1918
|
+
attemptId: record.attemptId as string,
|
|
1919
|
+
imageDigest: record.imageDigest as IWorkloadReplicaIdentity['imageDigest'],
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1897
1922
|
assertKeys(
|
|
1898
1923
|
record,
|
|
1899
1924
|
pathArg,
|
package/ts/data/coremail.ts
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* binding authority.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { IWorkloadReplicaIdentity } from './workloadreplicaidentity.js';
|
|
11
|
+
import type { TCorestoreDatabaseCredentialKey, TCorestoreObjectStorageCredentialSecretKey } from '../runtime.corestore.js';
|
|
12
|
+
import type { TObjectStorageEndpointEnvironmentKey } from '../platform/objectstorage.js';
|
|
13
|
+
|
|
10
14
|
export type TCoreMailSha256 = `sha256:${string}`;
|
|
11
15
|
|
|
12
16
|
export type TCoreMailCapability = 'outbound' | 'inbound';
|
|
@@ -293,6 +297,15 @@ export interface ICoreMailDesiredState {
|
|
|
293
297
|
cursorKeys: ICoreMailRuntimeKeyReference[];
|
|
294
298
|
}
|
|
295
299
|
|
|
300
|
+
/**
|
|
301
|
+
* The replica identity CoreMail before 34 reports, read from the `COREMAIL_*` variables its
|
|
302
|
+
* provisioner fabricated.
|
|
303
|
+
*
|
|
304
|
+
* @deprecated CoreMail 34 and later report `IWorkloadReplicaIdentity`, which its runtime injects
|
|
305
|
+
* (`workloadReplicaIdentityEnvironmentNames`); `taskId` is that identity's `attemptId`.
|
|
306
|
+
* `normalizeCoreMailReplicaIdentity` still reads this shape, exactly, from a CoreMail before 34;
|
|
307
|
+
* removed in the next major.
|
|
308
|
+
*/
|
|
296
309
|
export interface ICoreMailReplicaIdentity {
|
|
297
310
|
taskId: string;
|
|
298
311
|
serviceId: string;
|
|
@@ -314,7 +327,11 @@ export interface ICoreMailBindingReconciliationStatus {
|
|
|
314
327
|
}
|
|
315
328
|
|
|
316
329
|
export interface ICoreMailReconciliationStatus {
|
|
317
|
-
|
|
330
|
+
/**
|
|
331
|
+
* The reporting replica: `IWorkloadReplicaIdentity` from CoreMail 34 on, the deprecated
|
|
332
|
+
* `ICoreMailReplicaIdentity` from an earlier CoreMail. Each is read exactly.
|
|
333
|
+
*/
|
|
334
|
+
replica: IWorkloadReplicaIdentity | ICoreMailReplicaIdentity;
|
|
318
335
|
state: TCoreMailReconciliationState;
|
|
319
336
|
appliedConfigEpoch: number;
|
|
320
337
|
appliedDesiredStateDigest: TCoreMailSha256;
|
|
@@ -495,4 +512,28 @@ export const coreMailRuntimeKeys = Object.freeze({
|
|
|
495
512
|
controlBootstrap: 'COREMAIL_CONTROL_BOOTSTRAP',
|
|
496
513
|
controlCredentialSecret: 'COREMAIL_CONTROL_CREDENTIAL_SECRET',
|
|
497
514
|
gatewayCredentialSecret: 'COREMAIL_GATEWAY_CREDENTIAL',
|
|
515
|
+
/** Optional listener port inside the workload. */
|
|
516
|
+
port: 'COREMAIL_PORT',
|
|
517
|
+
/** Optional comma-separated canonical hostname list. */
|
|
518
|
+
hostnames: 'COREMAIL_HOSTNAMES',
|
|
519
|
+
/** Optional MongoDB connection pool bound. */
|
|
520
|
+
mongoDbMaxPoolSize: 'COREMAIL_MONGODB_MAX_POOL_SIZE',
|
|
498
521
|
} as const);
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The storage CoreMail 34 and later read, under the names its platform bindings deliver: the
|
|
525
|
+
* Corestore database binding's `MONGODB_URL` and `MONGODB_DATABASE`, the object-storage binding's
|
|
526
|
+
* `S3_ENDPOINT`, `S3_REGION` and `S3_BUCKET`, and that binding's `S3_ACCESS_KEY` and
|
|
527
|
+
* `S3_SECRET_KEY` secrets. They replace `COREMAIL_MONGODB_URL`, `COREMAIL_MONGODB_NAME`,
|
|
528
|
+
* `COREMAIL_STORAGE_DESCRIPTOR` and `COREMAIL_BUCKET_NAME`.
|
|
529
|
+
*/
|
|
530
|
+
export const coreMailStorageEnvironmentNames = Object.freeze({
|
|
531
|
+
databaseUrl: 'MONGODB_URL',
|
|
532
|
+
databaseName: 'MONGODB_DATABASE',
|
|
533
|
+
objectStorageEndpoint: 'S3_ENDPOINT',
|
|
534
|
+
objectStorageRegion: 'S3_REGION',
|
|
535
|
+
objectStorageBucket: 'S3_BUCKET',
|
|
536
|
+
objectStorageAccessKey: 'S3_ACCESS_KEY',
|
|
537
|
+
objectStorageSecretKey: 'S3_SECRET_KEY',
|
|
538
|
+
} as const satisfies Readonly<Record<string,
|
|
539
|
+
TCorestoreDatabaseCredentialKey | TObjectStorageEndpointEnvironmentKey | TCorestoreObjectStorageCredentialSecretKey>>);
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { readDataElements, readDataProperty, readExactDataRecord } from '../private/dataproperties.js';
|
|
2
|
+
import {
|
|
3
|
+
isReservedSecretKey, platformReservedSecretKeyPrefixes, runtimeInjectionSecretKeyPrefixes,
|
|
4
|
+
runtimeInjectionSecretKeys,
|
|
5
|
+
} from './secret.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A delegated administrator token (a "temp token") hands an agent a narrow, short-lived slice of
|
|
9
|
+
* one human administrator's authority, without the administrator's password or session.
|
|
10
|
+
*
|
|
11
|
+
* - Only a human administrator who signed in within five minutes mints one
|
|
12
|
+
* (`mintDelegatedAdminToken`), for scopes named explicitly from `delegatedAdminTokenScopes`.
|
|
13
|
+
* The plaintext token is answered exactly once; Cloudly stores only its hash.
|
|
14
|
+
* - It is exchanged with `getIdentityByToken` for a session that only the requests of its scopes
|
|
15
|
+
* (`delegatedAdminTokenScopeRequests`) admit. Every such request re-reads the token, so the
|
|
16
|
+
* session carries no validity of its own, and acts as `<administrator> via temp token <id>`.
|
|
17
|
+
* - It ends at its expiry, at most `maximumLifetimeMs` after the mint and never extended; when any
|
|
18
|
+
* administrator revokes it; or when the minting administrator's credential generation changes.
|
|
19
|
+
* - A delegated session never mints, lists or revokes tokens, and no scope reaches users, roles,
|
|
20
|
+
* human credentials, deployment tokens, authority records or organization membership.
|
|
21
|
+
*/
|
|
22
|
+
export const delegatedAdminTokenContract = Object.freeze({
|
|
23
|
+
/** The lifetime a mint that names none receives. */
|
|
24
|
+
defaultLifetimeMs: 60 * 60 * 1000,
|
|
25
|
+
/** The hard cap: a token lives at most one hour from its mint and is never extended. */
|
|
26
|
+
maximumLifetimeMs: 60 * 60 * 1000,
|
|
27
|
+
minimumLifetimeMs: 60 * 1000,
|
|
28
|
+
/** Every token is `delegatedAdminToken_<tokenId>_<secret>`, both parts 64 lowercase hex. */
|
|
29
|
+
tokenPrefix: 'delegatedAdminToken_',
|
|
30
|
+
/** Tokens one listing reports at most, newest first; a listing that leaves any out says so. */
|
|
31
|
+
maximumListedTokens: 256,
|
|
32
|
+
/** Secret key prefixes one token is granted at most. */
|
|
33
|
+
maximumSecretKeyPrefixes: 16,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What a token may be minted for. Each scope admits exactly the requests
|
|
38
|
+
* `delegatedAdminTokenScopeRequests` names for it, and nothing else does.
|
|
39
|
+
*
|
|
40
|
+
* - `secrets`: the operator-managed secrets of services and SecretSets: list and read their
|
|
41
|
+
* metadata and read the ingress recipient a value is sealed to; create, rotate, retire and revoke
|
|
42
|
+
* those whose key starts with a prefix the token was granted (`secretKeyPrefixes`, see
|
|
43
|
+
* `isDelegatedSecretKey`; `changeSecretLifecycle` only with the actions
|
|
44
|
+
* `delegatedAdminTokenSecretLifecycleActions` names, never `delete`). A token creates a secret
|
|
45
|
+
* only as a launcher-environment variable, never as a file. A token minted without prefixes
|
|
46
|
+
* creates, rotates, retires and revokes nothing.
|
|
47
|
+
* - `serviceCredentials`: a service's platform credential (`SERVEZONE_PLATFORM_AUTHORIZATION`):
|
|
48
|
+
* inspect, ensure, rotate and revoke it.
|
|
49
|
+
*/
|
|
50
|
+
export const delegatedAdminTokenScopes = ['secrets', 'serviceCredentials'] as const;
|
|
51
|
+
|
|
52
|
+
export type TDelegatedAdminTokenScope = typeof delegatedAdminTokenScopes[number];
|
|
53
|
+
|
|
54
|
+
/** The exact requests each scope admits a delegated session on, by their wire method names. */
|
|
55
|
+
export const delegatedAdminTokenScopeRequests = Object.freeze({
|
|
56
|
+
secrets: Object.freeze([
|
|
57
|
+
'getSecretIngressRecipient', 'listSecrets', 'getSecretMetadata', 'createSecret', 'rotateSecret',
|
|
58
|
+
'changeSecretLifecycle',
|
|
59
|
+
] as const),
|
|
60
|
+
serviceCredentials: Object.freeze(['getServiceMachineCredential', 'mutateServiceMachineCredential'] as const),
|
|
61
|
+
}) satisfies Readonly<Record<TDelegatedAdminTokenScope, readonly string[]>>;
|
|
62
|
+
|
|
63
|
+
/** A request one scope admits, by its wire method name. */
|
|
64
|
+
export type TDelegatedAdminTokenScopeRequest<TScope extends TDelegatedAdminTokenScope = TDelegatedAdminTokenScope> =
|
|
65
|
+
(typeof delegatedAdminTokenScopeRequests)[TScope][number];
|
|
66
|
+
|
|
67
|
+
/** Whether `scopeArg` admits the request a wire method names. */
|
|
68
|
+
export const isDelegatedAdminTokenScopeRequest = <TScope extends TDelegatedAdminTokenScope>(
|
|
69
|
+
scopeArg: TScope,
|
|
70
|
+
methodArg: unknown,
|
|
71
|
+
): methodArg is TDelegatedAdminTokenScopeRequest<TScope> => (
|
|
72
|
+
(delegatedAdminTokenScopeRequests[scopeArg] as readonly unknown[]).includes(methodArg)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The `changeSecretLifecycle` actions the `secrets` scope delegates, each only for a secret whose key
|
|
77
|
+
* the token's grant covers (`isDelegatedSecretKey`). Deleting a secret stays with a human
|
|
78
|
+
* administrator: a delegated session asking for it is refused like a request outside its scope.
|
|
79
|
+
*/
|
|
80
|
+
export const delegatedAdminTokenSecretLifecycleActions = Object.freeze(['retire', 'revoke'] as const);
|
|
81
|
+
|
|
82
|
+
/** One token, value-free. Neither the token nor its hash is ever part of it. */
|
|
83
|
+
export interface IDelegatedAdminTokenMetadata {
|
|
84
|
+
/** 64 lowercase hex; the part of the token that names it. */
|
|
85
|
+
tokenId: string;
|
|
86
|
+
/** The human administrator who minted the token and whom every use acts for. */
|
|
87
|
+
userId: string;
|
|
88
|
+
/** Distinct, in `delegatedAdminTokenScopes` order. */
|
|
89
|
+
scopes: TDelegatedAdminTokenScope[];
|
|
90
|
+
/**
|
|
91
|
+
* The secret key prefixes the token creates, rotates, retires and revokes secrets under, sorted
|
|
92
|
+
* and distinct; empty when it was granted none. Only a token with the `secrets` scope carries any.
|
|
93
|
+
*/
|
|
94
|
+
secretKeyPrefixes: string[];
|
|
95
|
+
createdAt: number;
|
|
96
|
+
expiresAt: number;
|
|
97
|
+
/** When an administrator revoked the token; `null` while it was never revoked. */
|
|
98
|
+
revokedAt: number | null;
|
|
99
|
+
/** The administrator who revoked it; `null` exactly when `revokedAt` is. */
|
|
100
|
+
revokedBy: string | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** What a mint states besides the administrator's identity. */
|
|
104
|
+
export interface IDelegatedAdminTokenMint {
|
|
105
|
+
scopes: TDelegatedAdminTokenScope[];
|
|
106
|
+
/** Absent: `defaultLifetimeMs`. Otherwise within `minimumLifetimeMs` and `maximumLifetimeMs`. */
|
|
107
|
+
lifetimeMs?: number;
|
|
108
|
+
/**
|
|
109
|
+
* The secret key prefixes the token may create, rotate, retire and revoke secrets under, each
|
|
110
|
+
* `isDelegatableSecretKeyPrefix`, distinct, at most `maximumSecretKeyPrefixes`; only with the
|
|
111
|
+
* `secrets` scope. Prefixes must be application-specific (`GITSYNC_`): a prefix that overlaps a
|
|
112
|
+
* reserved name is refused. Absent or empty: the token creates, rotates, retires and revokes
|
|
113
|
+
* nothing.
|
|
114
|
+
*/
|
|
115
|
+
secretKeyPrefixes?: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface IDelegatedAdminTokenMintResult {
|
|
119
|
+
/** The plaintext token, answered on this answer only; Cloudly keeps only its hash. */
|
|
120
|
+
token: string;
|
|
121
|
+
metadata: IDelegatedAdminTokenMetadata;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Every refusal the token requests name, as `IDelegatedAdminTokenErrorData`.
|
|
126
|
+
*
|
|
127
|
+
* - `ADMIN_AUTHORIZATION_REQUIRED`: a mint by anyone but a human administrator who signed in
|
|
128
|
+
* within five minutes, or a listing or revocation by anyone but an administrator. A delegated
|
|
129
|
+
* session is refused here on every request.
|
|
130
|
+
* - `DELEGATED_ADMIN_TOKEN_REQUEST_INVALID`: the request does not satisfy this contract.
|
|
131
|
+
* - `DELEGATED_ADMIN_TOKEN_NOT_FOUND`: no token has that id.
|
|
132
|
+
* - `DELEGATED_ADMIN_TOKEN_INACTIVE`: the token already ended: it expired, was revoked, or its
|
|
133
|
+
* administrator is no longer a human administrator at the credential generation it was minted
|
|
134
|
+
* under.
|
|
135
|
+
*/
|
|
136
|
+
export const delegatedAdminTokenRefusals = [
|
|
137
|
+
'ADMIN_AUTHORIZATION_REQUIRED',
|
|
138
|
+
'DELEGATED_ADMIN_TOKEN_REQUEST_INVALID',
|
|
139
|
+
'DELEGATED_ADMIN_TOKEN_NOT_FOUND',
|
|
140
|
+
'DELEGATED_ADMIN_TOKEN_INACTIVE',
|
|
141
|
+
] as const;
|
|
142
|
+
|
|
143
|
+
export type TDelegatedAdminTokenRefusal = typeof delegatedAdminTokenRefusals[number];
|
|
144
|
+
|
|
145
|
+
export interface IDelegatedAdminTokenErrorData {
|
|
146
|
+
code: TDelegatedAdminTokenRefusal;
|
|
147
|
+
retryable: false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fail = (): never => { throw new Error('Delegated admin token contract rejected.'); };
|
|
151
|
+
function requireValue(value: unknown): asserts value { if (!value) fail(); }
|
|
152
|
+
const safe = <T>(operation: () => T): T => { try { return operation(); } catch { return fail(); } };
|
|
153
|
+
/** Exactly this shape's keys, read once as data; an accessor is refused, never invoked. */
|
|
154
|
+
const object = (value: unknown, keys: readonly string[]): Record<string, unknown> => {
|
|
155
|
+
const copy = readExactDataRecord(value, keys);
|
|
156
|
+
requireValue(copy !== null);
|
|
157
|
+
return copy;
|
|
158
|
+
};
|
|
159
|
+
const identifier = (value: unknown): string => {
|
|
160
|
+
requireValue(typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(value));
|
|
161
|
+
return value;
|
|
162
|
+
};
|
|
163
|
+
const timestamp = (value: unknown): number => {
|
|
164
|
+
requireValue(typeof value === 'number' && Number.isSafeInteger(value) && value > 0);
|
|
165
|
+
return value;
|
|
166
|
+
};
|
|
167
|
+
const tokenIdPattern = /^[a-f0-9]{64}$/;
|
|
168
|
+
const tokenPattern = /^delegatedAdminToken_[a-f0-9]{64}_[a-f0-9]{64}$/;
|
|
169
|
+
|
|
170
|
+
/** Whether a value is a delegated admin token in its exact form; it proves nothing else. */
|
|
171
|
+
export const isDelegatedAdminToken = (value: unknown): value is string => (
|
|
172
|
+
typeof value === 'string' && tokenPattern.test(value)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/** Whether a value names a token: 64 lowercase hex. */
|
|
176
|
+
export const isDelegatedAdminTokenId = (value: unknown): value is string => (
|
|
177
|
+
typeof value === 'string' && tokenIdPattern.test(value)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const secretKeyPrefixPattern = /^[A-Z][A-Z0-9]*_$/;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Whether a prefix may be granted: `^[A-Z][A-Z0-9]*_$`, and overlapping no reserved name in
|
|
184
|
+
* either direction: it neither starts with nor is the start of an entry of
|
|
185
|
+
* `platformReservedSecretKeyPrefixes` or `runtimeInjectionSecretKeyPrefixes`, and no entry of
|
|
186
|
+
* `runtimeInjectionSecretKeys` starts with it. So a granted prefix is application-specific:
|
|
187
|
+
* `GIT_`, `NODE_`, `NPM_` and `AWS_` (`AWS_CA_BUNDLE`) are refused; `GITSYNC_` is not.
|
|
188
|
+
*/
|
|
189
|
+
export const isDelegatableSecretKeyPrefix = (value: unknown): value is string => (
|
|
190
|
+
typeof value === 'string' && value.length <= 64 && secretKeyPrefixPattern.test(value)
|
|
191
|
+
&& [...platformReservedSecretKeyPrefixes, ...runtimeInjectionSecretKeyPrefixes]
|
|
192
|
+
.every((reservedArg) => !value.startsWith(reservedArg) && !reservedArg.startsWith(value))
|
|
193
|
+
&& runtimeInjectionSecretKeys.every((reservedArg) => !reservedArg.startsWith(value))
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Whether a token granted `prefixesArg` creates, rotates, retires or revokes a secret of key
|
|
198
|
+
* `keyArg`, or delivers a launcher variable of that name: the key starts with a granted prefix and
|
|
199
|
+
* is not reserved (`isReservedSecretKey`).
|
|
200
|
+
*/
|
|
201
|
+
export const isDelegatedSecretKey = (prefixesArg: readonly string[], keyArg: string): boolean => (
|
|
202
|
+
prefixesArg.some((prefixArg) => keyArg.startsWith(prefixArg)) && !isReservedSecretKey(keyArg)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
/** Whether a lifetime is one a mint may state. */
|
|
206
|
+
export const isDelegatedAdminTokenLifetime = (value: unknown): value is number => (
|
|
207
|
+
typeof value === 'number' && Number.isSafeInteger(value)
|
|
208
|
+
&& value >= delegatedAdminTokenContract.minimumLifetimeMs
|
|
209
|
+
&& value <= delegatedAdminTokenContract.maximumLifetimeMs
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
/** Distinct known scopes, at least one, answered in `delegatedAdminTokenScopes` order. */
|
|
213
|
+
const scopes = (value: unknown, canonical: boolean): TDelegatedAdminTokenScope[] => {
|
|
214
|
+
const entries = readDataElements(value, delegatedAdminTokenScopes.length);
|
|
215
|
+
requireValue(entries !== null && entries.length > 0 && new Set(entries).size === entries.length
|
|
216
|
+
&& entries.every((entry) => delegatedAdminTokenScopes.some((scope) => scope === entry)));
|
|
217
|
+
const ordered = delegatedAdminTokenScopes.filter((scope) => entries.includes(scope));
|
|
218
|
+
requireValue(!canonical || ordered.every((scope, index) => entries[index] === scope));
|
|
219
|
+
return ordered;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** Distinct grantable prefixes, at most the cap, answered sorted; `canonical` requires them sorted. */
|
|
223
|
+
const secretKeyPrefixes = (value: unknown, canonical: boolean): string[] => {
|
|
224
|
+
const entries = readDataElements(value, delegatedAdminTokenContract.maximumSecretKeyPrefixes);
|
|
225
|
+
requireValue(entries !== null && new Set(entries).size === entries.length
|
|
226
|
+
&& entries.every((entry) => isDelegatableSecretKeyPrefix(entry)));
|
|
227
|
+
const prefixes = (entries as string[]).slice().sort();
|
|
228
|
+
requireValue(!canonical || prefixes.every((prefix, index) => entries[index] === prefix));
|
|
229
|
+
return prefixes;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Structural copy of a mint; the backend authorizes the administrator and enforces the cap. */
|
|
233
|
+
export const snapshotDelegatedAdminTokenMint = (value: unknown): IDelegatedAdminTokenMint => safe(() => {
|
|
234
|
+
const namesLifetime = readDataProperty(value, 'lifetimeMs') !== null;
|
|
235
|
+
const namesPrefixes = readDataProperty(value, 'secretKeyPrefixes') !== null;
|
|
236
|
+
const mint = object(value, ['scopes', ...(namesLifetime ? ['lifetimeMs'] : []),
|
|
237
|
+
...(namesPrefixes ? ['secretKeyPrefixes'] : [])]);
|
|
238
|
+
const copy: IDelegatedAdminTokenMint = { scopes: scopes(mint.scopes, false) };
|
|
239
|
+
if (namesLifetime) {
|
|
240
|
+
requireValue(isDelegatedAdminTokenLifetime(mint.lifetimeMs));
|
|
241
|
+
copy.lifetimeMs = mint.lifetimeMs;
|
|
242
|
+
}
|
|
243
|
+
if (namesPrefixes) {
|
|
244
|
+
const prefixes = secretKeyPrefixes(mint.secretKeyPrefixes, false);
|
|
245
|
+
requireValue(prefixes.length === 0 || copy.scopes.includes('secrets'));
|
|
246
|
+
copy.secretKeyPrefixes = prefixes;
|
|
247
|
+
}
|
|
248
|
+
return copy;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/** Structural copy of listed or answered metadata. */
|
|
252
|
+
export const snapshotDelegatedAdminTokenMetadata = (value: unknown): IDelegatedAdminTokenMetadata => safe(() => {
|
|
253
|
+
const metadata = object(value, ['tokenId', 'userId', 'scopes', 'secretKeyPrefixes', 'createdAt', 'expiresAt',
|
|
254
|
+
'revokedAt', 'revokedBy']);
|
|
255
|
+
requireValue(isDelegatedAdminTokenId(metadata.tokenId));
|
|
256
|
+
const createdAt = timestamp(metadata.createdAt);
|
|
257
|
+
const expiresAt = timestamp(metadata.expiresAt);
|
|
258
|
+
requireValue(expiresAt > createdAt
|
|
259
|
+
&& expiresAt - createdAt <= delegatedAdminTokenContract.maximumLifetimeMs);
|
|
260
|
+
const revoked = metadata.revokedAt !== null;
|
|
261
|
+
requireValue(revoked === (metadata.revokedBy !== null));
|
|
262
|
+
const revokedAt = revoked ? timestamp(metadata.revokedAt) : null;
|
|
263
|
+
requireValue(revokedAt === null || revokedAt >= createdAt);
|
|
264
|
+
const tokenScopes = scopes(metadata.scopes, true);
|
|
265
|
+
const prefixes = secretKeyPrefixes(metadata.secretKeyPrefixes, true);
|
|
266
|
+
requireValue(prefixes.length === 0 || tokenScopes.includes('secrets'));
|
|
267
|
+
return {
|
|
268
|
+
tokenId: metadata.tokenId, userId: identifier(metadata.userId), scopes: tokenScopes,
|
|
269
|
+
secretKeyPrefixes: prefixes, createdAt, expiresAt, revokedAt,
|
|
270
|
+
revokedBy: revoked ? identifier(metadata.revokedBy) : null,
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
/** Structural copy of a mint's answer: a token in its exact form and the metadata that names it. */
|
|
275
|
+
export const snapshotDelegatedAdminTokenMintResult = (value: unknown): IDelegatedAdminTokenMintResult => safe(() => {
|
|
276
|
+
const result = object(value, ['token', 'metadata']);
|
|
277
|
+
requireValue(isDelegatedAdminToken(result.token));
|
|
278
|
+
const metadata = snapshotDelegatedAdminTokenMetadata(result.metadata);
|
|
279
|
+
requireValue(result.token.slice(delegatedAdminTokenContract.tokenPrefix.length,
|
|
280
|
+
delegatedAdminTokenContract.tokenPrefix.length + 64) === metadata.tokenId && metadata.revokedAt === null);
|
|
281
|
+
return { token: result.token, metadata };
|
|
282
|
+
});
|
package/ts/data/gateway.ts
CHANGED
|
@@ -20,8 +20,16 @@ export type TManagedRouteKind = 'letsencrypt-http01-forward';
|
|
|
20
20
|
/**
|
|
21
21
|
* DNS handling requested alongside a gateway route synchronization.
|
|
22
22
|
*
|
|
23
|
-
* - 'reconcile'
|
|
24
|
-
*
|
|
23
|
+
* - 'reconcile' creates, updates and deletes the exact-host A or AAAA record
|
|
24
|
+
* the route's gateway DNS ownership holds. It never adopts, replaces or
|
|
25
|
+
* deletes a record without gateway DNS ownership. The route is satisfied by
|
|
26
|
+
* manual records only when no record at the name is owned and every manual A
|
|
27
|
+
* or AAAA record there matches the desired type, value and proxied flag (TTL
|
|
28
|
+
* ignored); anything else (a differing record, any CNAME, owned and manual
|
|
29
|
+
* records together) and an operator-protected hostname is a non-retryable
|
|
30
|
+
* `record-conflict` with no mutation. When the route stops claiming the
|
|
31
|
+
* hostname while another enabled route on the gateway serves it, the owned
|
|
32
|
+
* record is released to manual ownership instead of deleted.
|
|
25
33
|
* - 'observe' reports DNS state without mutating it.
|
|
26
34
|
* - 'skip' bypasses DNS inspection and mutation and is the default when omitted.
|
|
27
35
|
*/
|
|
@@ -29,6 +37,14 @@ export type TGatewayRouteDnsMode = 'reconcile' | 'observe' | 'skip';
|
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
39
|
* Detailed DNS outcome reported by a gateway route synchronization.
|
|
40
|
+
*
|
|
41
|
+
* - `manual-satisfied`: every manual A or AAAA record at the name matches the desired type,
|
|
42
|
+
* value and proxied flag; the gateway changes and claims nothing and reports them in
|
|
43
|
+
* `records` with `managed: false`.
|
|
44
|
+
* - `managed-released`: the route no longer claims the hostname, but another enabled route on
|
|
45
|
+
* the gateway still serves it, so the gateway released its record to manual ownership instead
|
|
46
|
+
* of deleting it; the released record is reported in `records` with `managed: false`.
|
|
47
|
+
* - `managed-overwrote-manual` and `managed-orphan-deleted`: deprecated; no gateway reports them.
|
|
32
48
|
*/
|
|
33
49
|
export type TGatewayRouteDnsStatus =
|
|
34
50
|
| 'not-applicable'
|
|
@@ -37,8 +53,10 @@ export type TGatewayRouteDnsStatus =
|
|
|
37
53
|
| 'managed-updated'
|
|
38
54
|
| 'managed-overwrote-manual'
|
|
39
55
|
| 'managed-unchanged'
|
|
56
|
+
| 'manual-satisfied'
|
|
40
57
|
| 'observed-satisfied'
|
|
41
58
|
| 'managed-deleted'
|
|
59
|
+
| 'managed-released'
|
|
42
60
|
| 'managed-orphan-deleted'
|
|
43
61
|
| 'record-missing'
|
|
44
62
|
| 'target-invalid'
|
|
@@ -77,7 +95,10 @@ export interface IGatewayRouteDnsResult {
|
|
|
77
95
|
zoneName?: string;
|
|
78
96
|
nameservers?: string[];
|
|
79
97
|
records?: IGatewayRouteDnsRecord[];
|
|
80
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Prior manual records replaced by this reconciliation, preserved as evidence.
|
|
100
|
+
* @deprecated No gateway replaces manual records; it accompanied `managed-overwrote-manual`.
|
|
101
|
+
*/
|
|
81
102
|
overwrittenRecords?: IGatewayRouteDnsRecord[];
|
|
82
103
|
/** Time at which the gateway checked the DNS state, as Unix epoch milliseconds. */
|
|
83
104
|
checkedAt: number;
|
package/ts/data/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './config.js';
|
|
|
9
9
|
export * from './corestore.js';
|
|
10
10
|
export * from './corestorecontroltoken.js';
|
|
11
11
|
export * from './corestorelegacyadoption.js';
|
|
12
|
+
export * from './delegatedadmintoken.js';
|
|
12
13
|
export * from './coremail.js';
|
|
13
14
|
export * from './coremail.runtime.js';
|
|
14
15
|
export * from './deployment.js';
|
|
@@ -90,3 +91,4 @@ export * from './traffic.js';
|
|
|
90
91
|
export * from './user.js';
|
|
91
92
|
export * from './version.js';
|
|
92
93
|
export * from './webpush.js';
|
|
94
|
+
export * from './workloadreplicaidentity.js';
|
package/ts/data/mail.ts
CHANGED
|
@@ -338,6 +338,54 @@ export interface IServiceMailConfig {
|
|
|
338
338
|
/** Caller-writable mail configuration. CoreMail binding metadata is excluded. */
|
|
339
339
|
export type TServiceMailWritableConfig = Omit<IServiceMailConfig, 'coreMail'>;
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* The environment names a service's default sender is delivered under, one spelling for the
|
|
343
|
+
* provisioners (Cloudly, Onebox) and the reader (`@serve.zone/platformclient`).
|
|
344
|
+
*
|
|
345
|
+
* Three groups, each delivered whole: the typed dcrouter-direct binding (`from`, `typedUrl`,
|
|
346
|
+
* `apiCredentialId`, `apiCredentialSecret`; `typedUrl` is `ICloudlySettings.dcrouterMailEndpointUrl`),
|
|
347
|
+
* its SMTP compatibility surface (`smtp*`), and a CoreMail binding (`coreMail*`, with `from`).
|
|
348
|
+
* `apiCredentialSecret`, `smtpPassword` and `coreMailCredentialSecret` are secrets and are
|
|
349
|
+
* delivered as secrets. Every further sender address uses the same names scoped to its address
|
|
350
|
+
* token (`createServiceMailAddressEnvironmentNames`).
|
|
351
|
+
*/
|
|
352
|
+
export const serviceMailEnvironmentNames = Object.freeze({
|
|
353
|
+
from: 'MAIL_FROM',
|
|
354
|
+
typedUrl: 'MAIL_TYPED_URL',
|
|
355
|
+
apiCredentialId: 'MAIL_API_CREDENTIAL_ID',
|
|
356
|
+
apiCredentialSecret: 'MAIL_API_CREDENTIAL_SECRET',
|
|
357
|
+
smtpHost: 'SMTP_HOST',
|
|
358
|
+
smtpPort: 'SMTP_PORT',
|
|
359
|
+
smtpTlsMode: 'SMTP_TLS_MODE',
|
|
360
|
+
smtpUsername: 'SMTP_USERNAME',
|
|
361
|
+
smtpPassword: 'SMTP_PASSWORD',
|
|
362
|
+
coreMailUrl: 'MAIL_COREMAIL_URL',
|
|
363
|
+
coreMailBindingId: 'MAIL_COREMAIL_BINDING_ID',
|
|
364
|
+
coreMailCredentialId: 'MAIL_COREMAIL_CREDENTIAL_ID',
|
|
365
|
+
coreMailCredentialVersionNumber: 'MAIL_COREMAIL_CREDENTIAL_VERSION',
|
|
366
|
+
coreMailCredentialSecret: 'MAIL_COREMAIL_CREDENTIAL_SECRET',
|
|
367
|
+
} as const);
|
|
368
|
+
|
|
369
|
+
export type TServiceMailEnvironmentNames = Readonly<Record<keyof typeof serviceMailEnvironmentNames, string>>;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The names one further sender address is delivered under: each of `serviceMailEnvironmentNames`
|
|
373
|
+
* with `MAIL_<token>_` in place of its `MAIL_` prefix, or before it where it has none
|
|
374
|
+
* (`MAIL_<token>_FROM`, `MAIL_<token>_TYPED_URL`, `MAIL_<token>_SMTP_HOST`,
|
|
375
|
+
* `MAIL_<token>_COREMAIL_URL`). The token is the address's upper-case token
|
|
376
|
+
* (`^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$`); any other token is the caller's error and is thrown.
|
|
377
|
+
*/
|
|
378
|
+
export const createServiceMailAddressEnvironmentNames = (addressTokenArg: string): TServiceMailEnvironmentNames => {
|
|
379
|
+
if (typeof addressTokenArg !== 'string' || !/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/.test(addressTokenArg)) {
|
|
380
|
+
throw new Error('service mail address token must be an upper-case address token');
|
|
381
|
+
}
|
|
382
|
+
const scoped: Record<string, string> = {};
|
|
383
|
+
for (const [key, name] of Object.entries(serviceMailEnvironmentNames)) {
|
|
384
|
+
scoped[key] = `MAIL_${addressTokenArg}_${name.startsWith('MAIL_') ? name.slice('MAIL_'.length) : name}`;
|
|
385
|
+
}
|
|
386
|
+
return Object.freeze(scoped) as TServiceMailEnvironmentNames;
|
|
387
|
+
};
|
|
388
|
+
|
|
341
389
|
export interface IServiceMailTargetAddressSummary {
|
|
342
390
|
address: string;
|
|
343
391
|
inbound: {
|
|
@@ -168,7 +168,10 @@ export type TRuntimeImagePullRefusalReason = typeof runtimeImagePullRefusalReaso
|
|
|
168
168
|
* Why a node refused to start a run before any runtime effect of it, as the node names it:
|
|
169
169
|
* `unsupported-readiness` a readiness probe the run's network cannot carry (a TCP or HTTP probe of
|
|
170
170
|
* a run without network authority), `unresolved-authority` a workload authority the node cannot
|
|
171
|
-
* resolve, and `invalid` an assignment or configuration the node cannot read
|
|
171
|
+
* resolve, and `invalid` an assignment or configuration the node cannot read or cannot run as stated,
|
|
172
|
+
* such as a config environment that states a name the runtime injects
|
|
173
|
+
* (`composeWorkloadReplicaEnvironment` answering `workload-identity-environment-conflict`, a
|
|
174
|
+
* service-status reason only Cloudly states).
|
|
172
175
|
*/
|
|
173
176
|
export const runtimeAssignmentRefusalReasons = ['unsupported-readiness', 'unresolved-authority', 'invalid'] as const;
|
|
174
177
|
export type TRuntimeAssignmentRefusalReason = typeof runtimeAssignmentRefusalReasons[number];
|
package/ts/data/secret.ts
CHANGED
|
@@ -276,6 +276,44 @@ export type TSecretDelivery = ISecretLauncherEnvironmentDelivery | ISecretFileDe
|
|
|
276
276
|
/** A delivery of a resolved secret manifest: every owner is stated. */
|
|
277
277
|
export type TSecretResolvedDelivery = ISecretResolvedLauncherEnvironmentDelivery | ISecretFileDelivery;
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Prefixes of the names the platform injects into a workload itself (`SERVEZONE_*`): a secret of
|
|
281
|
+
* such a name would replace what the platform delivers.
|
|
282
|
+
*/
|
|
283
|
+
export const platformReservedSecretKeyPrefixes = Object.freeze(['SERVEZONE_'] as const);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Prefixes of the names a loader, language runtime, package manager or library reads to change
|
|
287
|
+
* what a process loads, runs or trusts (`LD_PRELOAD`, `NODE_OPTIONS`, `PYTHONPATH`, `OPENSSL_CONF`,
|
|
288
|
+
* `DOTNET_STARTUP_HOOKS`, `NPM_CONFIG_REGISTRY`, …). Matched as plain prefixes, so `PYTHON`, `PERL`,
|
|
289
|
+
* `RUBY` and `PHP` cover every name that starts with them.
|
|
290
|
+
*/
|
|
291
|
+
export const runtimeInjectionSecretKeyPrefixes = Object.freeze([
|
|
292
|
+
'LD_', 'DYLD_', 'NODE_', 'DENO_', 'BUN_', 'PYTHON', 'PERL', 'RUBY', 'PHP', 'JAVA_', 'JDK_', '_JAVA_', 'DOTNET_',
|
|
293
|
+
'CORECLR_', 'LUA_', 'GEM_', 'BUNDLE_', 'NPM_CONFIG_', 'PIP_', 'GLIBC_', 'MALLOC_', 'GCONV_', 'OPENSSL_', 'SSL_',
|
|
294
|
+
'CURL_', 'REQUESTS_', 'GIT_',
|
|
295
|
+
] as const);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Exact names a shell, loader, runtime or HTTP client reads to change where a process looks,
|
|
299
|
+
* connects or what it trusts or records (`CLASSPATH`, `AWS_CA_BUNDLE`, `SSLKEYLOGFILE`).
|
|
300
|
+
*/
|
|
301
|
+
export const runtimeInjectionSecretKeys = Object.freeze([
|
|
302
|
+
'PATH', 'HOME', 'SHELL', 'ENV', 'BASH_ENV', 'IFS', 'TMPDIR', 'LOCPATH', 'NLSPATH', 'CLASSPATH', 'HTTP_PROXY',
|
|
303
|
+
'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'FTP_PROXY', 'AWS_CA_BUNDLE', 'SSLKEYLOGFILE',
|
|
304
|
+
] as const);
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether a secret key, or a name a secret is delivered under, is reserved: one of
|
|
308
|
+
* `runtimeInjectionSecretKeys`, or starting with an entry of `platformReservedSecretKeyPrefixes`
|
|
309
|
+
* or `runtimeInjectionSecretKeyPrefixes`. A delegated administrator token never writes such a name.
|
|
310
|
+
*/
|
|
311
|
+
export const isReservedSecretKey = (keyArg: string): boolean => (
|
|
312
|
+
(runtimeInjectionSecretKeys as readonly string[]).includes(keyArg)
|
|
313
|
+
|| platformReservedSecretKeyPrefixes.some((prefixArg) => keyArg.startsWith(prefixArg))
|
|
314
|
+
|| runtimeInjectionSecretKeyPrefixes.some((prefixArg) => keyArg.startsWith(prefixArg))
|
|
315
|
+
);
|
|
316
|
+
|
|
279
317
|
export const secretValueLimits = Object.freeze({
|
|
280
318
|
maximumBytes: 500 * 1024,
|
|
281
319
|
minimumGeneratedBytes: 16,
|
|
@@ -93,7 +93,7 @@ const serviceRuntimeRefusals = [
|
|
|
93
93
|
'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
|
|
94
94
|
'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
|
|
95
95
|
'published-port-reserved', 'published-port-symmetric-without-egress', 'readiness-network-unsupported',
|
|
96
|
-
'cluster-relay-missing', 'cluster-ingress-environment-conflict',
|
|
96
|
+
'cluster-relay-missing', 'cluster-ingress-environment-conflict', 'workload-identity-environment-conflict',
|
|
97
97
|
] as const;
|
|
98
98
|
|
|
99
99
|
/**
|
|
@@ -183,6 +183,19 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
183
183
|
* `cluster-ingress-environment-conflict` is an ingress service whose own environment already sets
|
|
184
184
|
* one of `clusterIngressEnvironmentNames`: Cloudly owns those names, and a service value would
|
|
185
185
|
* otherwise silently replace Cloudly's or be replaced by it.
|
|
186
|
+
*
|
|
187
|
+
* `workload-identity-environment-conflict` is a service whose effective environment, or one of
|
|
188
|
+
* whose launcher-environment secrets, states one of `workloadReplicaIdentityEnvironmentNames`
|
|
189
|
+
* (`findWorkloadReplicaIdentityEnvironmentConflicts`): every runtime injects those names into every
|
|
190
|
+
* workload (`composeWorkloadReplicaEnvironment`), so a service value would silently replace the
|
|
191
|
+
* replica's identity or be replaced by it. Cloudly alone states it, and only in this status, which
|
|
192
|
+
* it stores and answers on `getServiceRuntimeSpec`, `getServiceRuntimeSpecForCluster` and
|
|
193
|
+
* `getServiceRuntimeSpecs`. Those requests carry no protocol offer, so the reason cannot be gated
|
|
194
|
+
* on a peer's version, and no other refusal names this check. A reader built before the release
|
|
195
|
+
* that added it refuses such a status whole, so every reader of statuses moves to that release
|
|
196
|
+
* before Cloudly does, and a Cloudly that has stored one is not rolled back below it. A node never
|
|
197
|
+
* states it: a Pallet whose composition refuses reports the run as the existing
|
|
198
|
+
* `TRuntimeAssignmentFailureCause` `refused` with `invalid`.
|
|
186
199
|
*/
|
|
187
200
|
export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
|
|
188
201
|
|