@serve.zone/interfaces 32.28.0 → 32.29.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/changelog.md +32 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.d.ts +1 -1
- package/dist_ts/data/cluster.js +6 -1
- package/dist_ts/data/networkaddressplan.d.ts +27 -3
- package/dist_ts/data/networkaddressplan.js +38 -2
- package/dist_ts/data/secret.d.ts +11 -0
- package/dist_ts/data/secret.js +13 -1
- package/dist_ts/data/settings.d.ts +51 -0
- package/dist_ts/data/settings.js +83 -1
- package/dist_ts/private/networkcontract.d.ts +18 -0
- package/dist_ts/private/networkcontract.js +23 -6
- package/dist_ts/requests/network.d.ts +2 -1
- package/dist_ts/requests/settings.d.ts +56 -2
- package/dist_ts/requests/settings.js +91 -1
- package/package.json +1 -1
- package/readme.md +87 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +5 -0
- package/ts/data/networkaddressplan.ts +66 -4
- package/ts/data/secret.ts +16 -0
- package/ts/data/settings.ts +127 -0
- package/ts/private/networkcontract.ts +36 -1
- package/ts/requests/network.ts +2 -1
- package/ts/requests/settings.ts +162 -2
package/ts/data/settings.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TServiceMailSubmissionTlsMode } from './mail.js';
|
|
2
2
|
import type { ISystemSecretOwner, TSecretManagementScope } from './secret.js';
|
|
3
|
+
import { hasExactKeys, readDataProperties } from '../private/dataproperties.js';
|
|
3
4
|
|
|
4
5
|
export interface ICloudlyS3BackupTargetDescriptor {
|
|
5
6
|
type: 's3';
|
|
@@ -109,3 +110,129 @@ export const cloudlySystemSecretKeys = Object.freeze({
|
|
|
109
110
|
coreMailSmtpTlsCertificatePem: 'COREMAIL_SMTP_TLS_CERTIFICATE_PEM',
|
|
110
111
|
coreMailSmtpTlsPrivateKeyPem: 'COREMAIL_SMTP_TLS_PRIVATE_KEY_PEM',
|
|
111
112
|
} as const);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How far Cloudly's enrollment as a dcrouter gateway client has come.
|
|
116
|
+
*
|
|
117
|
+
* - `unconfigured` — no `dcrouterGatewayUrl` is set, so there is nothing to enroll with.
|
|
118
|
+
* - `checking` — Cloudly is validating or provisioning its credential right now.
|
|
119
|
+
* - `ready` — Cloudly holds a validated gateway-client credential whose policy matches its settings.
|
|
120
|
+
* - `degraded` — dcrouter could not be reached; Cloudly retries on its own.
|
|
121
|
+
* - `admin-bootstrap-required` — the held credential is missing, rejected or stale (its policy no
|
|
122
|
+
* longer matches Cloudly's settings or the runtime network address plan's DNS record allowance),
|
|
123
|
+
* and only a dcrouter admin token handed in with `bootstrapExternalGatewayCredential` moves it on.
|
|
124
|
+
*/
|
|
125
|
+
export type TExternalGatewayEnrollmentStatus =
|
|
126
|
+
| 'unconfigured'
|
|
127
|
+
| 'checking'
|
|
128
|
+
| 'ready'
|
|
129
|
+
| 'degraded'
|
|
130
|
+
| 'admin-bootstrap-required';
|
|
131
|
+
|
|
132
|
+
/** Cloudly's gateway-client enrollment as an administrator reads it; it never carries a credential. */
|
|
133
|
+
export interface IExternalGatewayEnrollmentState {
|
|
134
|
+
status: TExternalGatewayEnrollmentStatus;
|
|
135
|
+
/** Why the state is what it is, in Cloudly's words; absent when there is nothing to say. */
|
|
136
|
+
reason?: string;
|
|
137
|
+
/** When Cloudly last finished a check, in milliseconds since the epoch. */
|
|
138
|
+
lastCheckedAt?: number;
|
|
139
|
+
/** When Cloudly retries on its own, for a `degraded` state. */
|
|
140
|
+
nextRetryAt?: number;
|
|
141
|
+
/** Whether Cloudly still holds a validated runtime credential it can serve gateway calls with. */
|
|
142
|
+
cachedValidationProof: boolean;
|
|
143
|
+
/** Whether a provisioned candidate credential still awaits finalization at dcrouter. */
|
|
144
|
+
finalizationPending: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const externalGatewayEnrollmentStatuses = Object.freeze([
|
|
148
|
+
'unconfigured',
|
|
149
|
+
'checking',
|
|
150
|
+
'ready',
|
|
151
|
+
'degraded',
|
|
152
|
+
'admin-bootstrap-required',
|
|
153
|
+
] as const satisfies readonly TExternalGatewayEnrollmentStatus[]);
|
|
154
|
+
|
|
155
|
+
export const externalGatewayBootstrapContract = Object.freeze({
|
|
156
|
+
/**
|
|
157
|
+
* The longest admin bootstrap token Cloudly accepts, in bytes. A dcrouter API token is far
|
|
158
|
+
* shorter; the bound only keeps an unopened envelope from carrying an arbitrary payload.
|
|
159
|
+
*/
|
|
160
|
+
maximumTokenBytes: 1_024,
|
|
161
|
+
/** The longest `reason` an enrollment state carries, in characters. */
|
|
162
|
+
maximumReasonLength: 4_096,
|
|
163
|
+
/** The longest `expectedGatewayUrl` a bootstrap request states, in characters. */
|
|
164
|
+
maximumGatewayUrlLength: 2_048,
|
|
165
|
+
/** The longest `expectedGatewayClientId` a bootstrap request states, in characters. */
|
|
166
|
+
maximumGatewayClientIdLength: 253,
|
|
167
|
+
} as const);
|
|
168
|
+
|
|
169
|
+
/** Every reason Cloudly answers instead of bootstrapping its gateway-client credential. */
|
|
170
|
+
export const externalGatewayBootstrapRefusals = Object.freeze([
|
|
171
|
+
/** Cloudly has no `dcrouterGatewayUrl` or no `dcrouterGatewayClientId` to enroll under. */
|
|
172
|
+
'gateway-unconfigured',
|
|
173
|
+
/**
|
|
174
|
+
* The request's `expectedGatewayUrl` or `expectedGatewayClientId` is not what Cloudly's settings
|
|
175
|
+
* state, so the token would reach a dcrouter or a client the administrator did not name.
|
|
176
|
+
*/
|
|
177
|
+
'gateway-target-mismatch',
|
|
178
|
+
/** The sealed token does not open, or what it opens to breaks the bootstrap token rule. */
|
|
179
|
+
'bootstrap-token-invalid',
|
|
180
|
+
/** dcrouter did not accept the token at all: unknown, revoked, disabled or expired. */
|
|
181
|
+
'bootstrap-token-rejected',
|
|
182
|
+
/** dcrouter accepted the token, but its role is not `admin`, so it cannot provision a client. */
|
|
183
|
+
'bootstrap-token-not-admin',
|
|
184
|
+
/** A candidate credential from an earlier provisioning is still being finalized; retry later. */
|
|
185
|
+
'bootstrap-candidate-pending',
|
|
186
|
+
/** dcrouter could not be reached; the held credential is unchanged and the call may be repeated. */
|
|
187
|
+
'gateway-unreachable',
|
|
188
|
+
/** dcrouter refused provisioning, or the credential it issued failed Cloudly's validation. */
|
|
189
|
+
'bootstrap-provisioning-refused',
|
|
190
|
+
] as const);
|
|
191
|
+
|
|
192
|
+
export type TExternalGatewayBootstrapRefusal = typeof externalGatewayBootstrapRefusals[number];
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The bootstrap token rule, judged on the opened bytes: 1 to `maximumTokenBytes` bytes of visible
|
|
196
|
+
* ASCII, with no whitespace or control character. The token is dcrouter's; this only refuses what
|
|
197
|
+
* could never be one, so a pasted line break or an empty pipe never reaches dcrouter.
|
|
198
|
+
*/
|
|
199
|
+
export const isExternalGatewayBootstrapToken = (valueArg: unknown): valueArg is Uint8Array => (
|
|
200
|
+
valueArg instanceof Uint8Array
|
|
201
|
+
&& valueArg.length > 0
|
|
202
|
+
&& valueArg.length <= externalGatewayBootstrapContract.maximumTokenBytes
|
|
203
|
+
&& valueArg.every((byteArg) => byteArg >= 0x21 && byteArg <= 0x7e)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const isEpochMilliseconds = (valueArg: unknown): boolean => (
|
|
207
|
+
Number.isSafeInteger(valueArg) && (valueArg as number) >= 0
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
/** Structural check of an enrollment state as a client receives it; every problem is named. */
|
|
211
|
+
export const validateExternalGatewayEnrollmentState = (valueArg: unknown): string[] => {
|
|
212
|
+
const state = readDataProperties(valueArg);
|
|
213
|
+
if (state === null) return ['external gateway enrollment state must be a plain record'];
|
|
214
|
+
const errors: string[] = [];
|
|
215
|
+
if (!hasExactKeys(state, ['status', 'cachedValidationProof', 'finalizationPending'],
|
|
216
|
+
['reason', 'lastCheckedAt', 'nextRetryAt'])) {
|
|
217
|
+
errors.push('external gateway enrollment state contains fields outside its schema');
|
|
218
|
+
}
|
|
219
|
+
if (!externalGatewayEnrollmentStatuses.some((statusArg) => statusArg === state.status)) {
|
|
220
|
+
errors.push('external gateway enrollment status is unknown');
|
|
221
|
+
}
|
|
222
|
+
if (state.reason !== undefined && (typeof state.reason !== 'string'
|
|
223
|
+
|| state.reason.length === 0
|
|
224
|
+
|| state.reason.length > externalGatewayBootstrapContract.maximumReasonLength)) {
|
|
225
|
+
errors.push('external gateway enrollment reason must be a bounded non-empty string');
|
|
226
|
+
}
|
|
227
|
+
for (const key of ['lastCheckedAt', 'nextRetryAt'] as const) {
|
|
228
|
+
if (state[key] !== undefined && !isEpochMilliseconds(state[key])) {
|
|
229
|
+
errors.push(`external gateway enrollment ${key} must be epoch milliseconds`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const key of ['cachedValidationProof', 'finalizationPending'] as const) {
|
|
233
|
+
if (typeof state[key] !== 'boolean') {
|
|
234
|
+
errors.push(`external gateway enrollment ${key} must be a boolean`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return errors;
|
|
238
|
+
};
|
|
@@ -9,7 +9,42 @@ export const failNetwork = (): never => { throw new Error('Runtime network proje
|
|
|
9
9
|
export function requireNetwork(value: unknown): asserts value { if (!value) failNetwork(); }
|
|
10
10
|
export const networkCanonical = (value: unknown): string => canonicalizeStrictJson(value, failNetwork);
|
|
11
11
|
export const networkEqual = (left: unknown, right: unknown): boolean => networkCanonical(left) === networkCanonical(right);
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
/** The runtime network address plan's lists that must be stated in their one canonical order. */
|
|
14
|
+
export type TRuntimeNetworkAddressPlanOrderedField = 'prefixes' | 'pools' | 'resolvers' | 'platformEndpoints';
|
|
15
|
+
|
|
16
|
+
/** The structure rules an address plan refusal names; `order` is the only one named so far. */
|
|
17
|
+
export type TRuntimeNetworkAddressPlanStructureRule = 'order';
|
|
18
|
+
|
|
19
|
+
/** Which list of a plan broke which structure rule; never a value from the plan. */
|
|
20
|
+
export interface IRuntimeNetworkAddressPlanStructureViolation {
|
|
21
|
+
field: TRuntimeNetworkAddressPlanOrderedField;
|
|
22
|
+
rule: TRuntimeNetworkAddressPlanStructureRule;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A plan refused for a structure rule the contract names, rather than with the contract's generic
|
|
27
|
+
* rejection. It is the one error `networkSafe` lets through, so a caller can tell an administrator
|
|
28
|
+
* which list to fix; its message names the field and the rule and quotes nothing from the plan.
|
|
29
|
+
*/
|
|
30
|
+
export class RuntimeNetworkAddressPlanStructureError extends Error {
|
|
31
|
+
public readonly violation: IRuntimeNetworkAddressPlanStructureViolation;
|
|
32
|
+
|
|
33
|
+
public constructor(violationArg: IRuntimeNetworkAddressPlanStructureViolation) {
|
|
34
|
+
super(`Runtime network address plan rejected: ${violationArg.field} breaks the ${violationArg.rule} rule.`);
|
|
35
|
+
this.name = 'RuntimeNetworkAddressPlanStructureError';
|
|
36
|
+
this.violation = { field: violationArg.field, rule: violationArg.rule };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const networkSafe = <T>(operation: () => T): T => {
|
|
41
|
+
try {
|
|
42
|
+
return operation();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof RuntimeNetworkAddressPlanStructureError) throw error;
|
|
45
|
+
return failNetwork();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
13
48
|
|
|
14
49
|
/** Bound decoded transport input before canonicalization; never execute getters/toJSON. */
|
|
15
50
|
export const captureNetwork = (value: unknown): unknown => networkSafe(() => {
|
package/ts/requests/network.ts
CHANGED
|
@@ -108,7 +108,8 @@ export interface IRequest_Any_Cloudly_GetNetworkNodes {
|
|
|
108
108
|
* validates the plan with `snapshotRuntimeNetworkAddressPlan`, decides the change with
|
|
109
109
|
* `admitRuntimeNetworkAddressPlanChange`, refuses the change while a resolver or platform endpoint
|
|
110
110
|
* it removes or changes is still referenced, and answers with the stored plan: at its new revision,
|
|
111
|
-
* or unchanged.
|
|
111
|
+
* or unchanged. A list stated out of its canonical order (see `IRuntimeNetworkAddressPlan`) is
|
|
112
|
+
* refused `plan-unsorted`, naming the list as the refusal's reference; the plan is never reordered.
|
|
112
113
|
*/
|
|
113
114
|
export interface IReq_Any_Cloudly_SetRuntimeNetworkAddressPlan extends plugins.typedrequestInterfaces.implementsTR<
|
|
114
115
|
plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_SetRuntimeNetworkAddressPlan
|
package/ts/requests/settings.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import * as secretData from '../data/secret.js';
|
|
3
|
+
import * as settingsData from '../data/settings.js';
|
|
4
|
+
import type { IActiveSecretRecipientMetadata } from '../data/secret.js';
|
|
5
|
+
import type { ICloudlySettings, IExternalGatewayEnrollmentState } from '../data/settings.js';
|
|
6
|
+
import type { IIdentity, IIdentityCredential } from '../data/user.js';
|
|
7
|
+
import { readDataProperties, hasExactKeys } from '../private/dataproperties.js';
|
|
4
8
|
|
|
5
9
|
// Get Settings
|
|
6
10
|
export interface IRequest_GetSettings extends plugins.typedrequestInterfaces.ITypedRequest {
|
|
@@ -64,3 +68,159 @@ export interface IRequest_GetSetting extends plugins.typedrequestInterfaces.ITyp
|
|
|
64
68
|
value: ICloudlySettings[keyof ICloudlySettings] | undefined;
|
|
65
69
|
};
|
|
66
70
|
}
|
|
71
|
+
|
|
72
|
+
/** A dcrouter admin bootstrap token, sealed to Cloudly's active secret ingress recipient. */
|
|
73
|
+
export interface IExternalGatewayBootstrapTokenInput {
|
|
74
|
+
mode: 'sealed';
|
|
75
|
+
/** Sealed under `data.createGatewayBootstrapEnvelopeContext` for the request's expected target. */
|
|
76
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enrolls Cloudly as its dcrouter gateway client with a dcrouter admin token an administrator
|
|
81
|
+
* hands in once.
|
|
82
|
+
*
|
|
83
|
+
* Cloudly opens the token, checks the target the request names against its own settings, asks
|
|
84
|
+
* dcrouter whether the token is an admin token, and provisions, finalizes and stores its own
|
|
85
|
+
* least-privilege gateway-client credential with it — under the policy its settings and the runtime
|
|
86
|
+
* network address plan state now. The admin token is used for this one call and never stored; the
|
|
87
|
+
* administrator revokes it at dcrouter afterwards. A refusal leaves the credential Cloudly already
|
|
88
|
+
* holds unchanged and is answered with one of `data.externalGatewayBootstrapRefusals`.
|
|
89
|
+
*
|
|
90
|
+
* This is the one way a gateway credential enters Cloudly, for a first enrollment and for the
|
|
91
|
+
* re-bootstrap a changed address plan or gateway setting asks for (`admin-bootstrap-required`).
|
|
92
|
+
*/
|
|
93
|
+
export interface IReq_Any_Cloudly_BootstrapExternalGatewayCredential
|
|
94
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
95
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
96
|
+
IReq_Any_Cloudly_BootstrapExternalGatewayCredential
|
|
97
|
+
> {
|
|
98
|
+
method: 'bootstrapExternalGatewayCredential';
|
|
99
|
+
request: {
|
|
100
|
+
identity: IIdentityCredential;
|
|
101
|
+
/** The `dcrouterGatewayUrl` the administrator read from Cloudly's settings. */
|
|
102
|
+
expectedGatewayUrl: string;
|
|
103
|
+
/** The `dcrouterGatewayClientId` the administrator read from Cloudly's settings. */
|
|
104
|
+
expectedGatewayClientId: string;
|
|
105
|
+
bootstrapToken: IExternalGatewayBootstrapTokenInput;
|
|
106
|
+
};
|
|
107
|
+
response: {
|
|
108
|
+
/** The enrollment after the bootstrap: `ready` when the new credential is Cloudly's runtime authority. */
|
|
109
|
+
state: IExternalGatewayEnrollmentState;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Cloudly's gateway-client enrollment state, for an administrator; it never carries a credential. */
|
|
114
|
+
export interface IReq_Any_Cloudly_GetExternalGatewayEnrollmentState
|
|
115
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
116
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
117
|
+
IReq_Any_Cloudly_GetExternalGatewayEnrollmentState
|
|
118
|
+
> {
|
|
119
|
+
method: 'getExternalGatewayEnrollmentState';
|
|
120
|
+
request: {
|
|
121
|
+
identity: IIdentityCredential;
|
|
122
|
+
};
|
|
123
|
+
response: {
|
|
124
|
+
state: IExternalGatewayEnrollmentState;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const hasJwtIdentity = (valueArg: unknown): boolean => {
|
|
129
|
+
const identity = readDataProperties(valueArg);
|
|
130
|
+
return identity !== null && typeof identity.jwt === 'string' && identity.jwt.length > 0;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Structural check of the target a bootstrap request names; empty when both are well formed. */
|
|
134
|
+
const validateExpectedGatewayTarget = (urlArg: unknown, clientIdArg: unknown): string[] => {
|
|
135
|
+
const errors: string[] = [];
|
|
136
|
+
let url: URL | undefined;
|
|
137
|
+
if (typeof urlArg === 'string'
|
|
138
|
+
&& urlArg.length > 0
|
|
139
|
+
&& urlArg.length <= settingsData.externalGatewayBootstrapContract.maximumGatewayUrlLength) {
|
|
140
|
+
try {
|
|
141
|
+
url = new URL(urlArg);
|
|
142
|
+
} catch {
|
|
143
|
+
url = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!url
|
|
147
|
+
|| url.protocol !== 'https:'
|
|
148
|
+
|| url.username !== ''
|
|
149
|
+
|| url.password !== ''
|
|
150
|
+
|| url.search !== ''
|
|
151
|
+
|| url.hash !== '') {
|
|
152
|
+
errors.push('bootstrap expectedGatewayUrl must be an HTTPS URL without credentials, query or fragment');
|
|
153
|
+
}
|
|
154
|
+
if (typeof clientIdArg !== 'string'
|
|
155
|
+
|| clientIdArg.length === 0
|
|
156
|
+
|| clientIdArg.length > settingsData.externalGatewayBootstrapContract.maximumGatewayClientIdLength
|
|
157
|
+
|| !/^[\x21-\x7e]+$/.test(clientIdArg)) {
|
|
158
|
+
errors.push('bootstrap expectedGatewayClientId must be a bounded identifier without whitespace');
|
|
159
|
+
}
|
|
160
|
+
return errors;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Checks a bootstrap request against its schema and the active ingress recipient, without opening
|
|
165
|
+
* the envelope: the recipient, the context digest of the named target and the ciphertext bound of
|
|
166
|
+
* `data.externalGatewayBootstrapContract.maximumTokenBytes`. Every problem is named.
|
|
167
|
+
*/
|
|
168
|
+
export const validateBootstrapExternalGatewayCredentialRequest = async (
|
|
169
|
+
requestArg: unknown,
|
|
170
|
+
activeIngressRecipientArg: IActiveSecretRecipientMetadata,
|
|
171
|
+
): Promise<string[]> => {
|
|
172
|
+
const request = readDataProperties(requestArg);
|
|
173
|
+
if (request === null) return ['bootstrap request must be a plain record'];
|
|
174
|
+
const errors: string[] = [];
|
|
175
|
+
if (!hasExactKeys(request, ['identity', 'expectedGatewayUrl', 'expectedGatewayClientId', 'bootstrapToken'])) {
|
|
176
|
+
errors.push('bootstrap request contains fields outside its schema');
|
|
177
|
+
}
|
|
178
|
+
if (!hasJwtIdentity(request.identity)) {
|
|
179
|
+
errors.push('bootstrap identity must be a JWT credential');
|
|
180
|
+
}
|
|
181
|
+
const targetErrors = validateExpectedGatewayTarget(
|
|
182
|
+
request.expectedGatewayUrl,
|
|
183
|
+
request.expectedGatewayClientId,
|
|
184
|
+
);
|
|
185
|
+
errors.push(...targetErrors);
|
|
186
|
+
const tokenInput = readDataProperties(request.bootstrapToken);
|
|
187
|
+
if (tokenInput === null || tokenInput.mode !== 'sealed') {
|
|
188
|
+
errors.push('bootstrap token must be a sealed input');
|
|
189
|
+
return errors;
|
|
190
|
+
}
|
|
191
|
+
const expectedContext = targetErrors.length === 0
|
|
192
|
+
? secretData.createGatewayBootstrapEnvelopeContext({
|
|
193
|
+
gatewayUrl: request.expectedGatewayUrl as string,
|
|
194
|
+
gatewayClientId: request.expectedGatewayClientId as string,
|
|
195
|
+
})
|
|
196
|
+
: new Uint8Array();
|
|
197
|
+
errors.push(...await secretData.validateSecretValueInputForRecipient(
|
|
198
|
+
tokenInput,
|
|
199
|
+
activeIngressRecipientArg,
|
|
200
|
+
expectedContext,
|
|
201
|
+
));
|
|
202
|
+
const envelope = readDataProperties(tokenInput.envelope);
|
|
203
|
+
if (envelope !== null
|
|
204
|
+
&& typeof envelope.ciphertext === 'string'
|
|
205
|
+
&& envelope.ciphertext.length
|
|
206
|
+
> Math.ceil(settingsData.externalGatewayBootstrapContract.maximumTokenBytes * 4 / 3)) {
|
|
207
|
+
errors.push(
|
|
208
|
+
`bootstrap token must not exceed ${settingsData.externalGatewayBootstrapContract.maximumTokenBytes} bytes`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return errors;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Checks an enrollment state read; every problem is named. */
|
|
215
|
+
export const validateGetExternalGatewayEnrollmentStateRequest = (requestArg: unknown): string[] => {
|
|
216
|
+
const request = readDataProperties(requestArg);
|
|
217
|
+
if (request === null) return ['enrollment state request must be a plain record'];
|
|
218
|
+
const errors: string[] = [];
|
|
219
|
+
if (!hasExactKeys(request, ['identity'])) {
|
|
220
|
+
errors.push('enrollment state request contains fields outside its schema');
|
|
221
|
+
}
|
|
222
|
+
if (!hasJwtIdentity(request.identity)) {
|
|
223
|
+
errors.push('enrollment state identity must be a JWT credential');
|
|
224
|
+
}
|
|
225
|
+
return errors;
|
|
226
|
+
};
|