@emilia-protocol/gate 0.9.5 → 0.10.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 +70 -0
- package/LICENSE +190 -0
- package/README.md +75 -16
- package/action-control-manifest.js +26 -0
- package/adapters/_kit.js +47 -5
- package/adapters/aws.js +15 -4
- package/adapters/github-demo.mjs +25 -22
- package/adapters/github.js +1 -1
- package/adapters/jira.js +1 -0
- package/adapters/linear.js +1 -0
- package/adapters/salesforce.js +1 -0
- package/adapters/stripe.js +1 -1
- package/adapters/supabase.js +26 -4
- package/adapters/vercel.js +33 -4
- package/aec-execution.js +1 -3
- package/breakglass.js +285 -51
- package/control-plane.js +341 -0
- package/coverage.js +722 -0
- package/custody-demo.mjs +13 -3
- package/demo.mjs +9 -3
- package/deploy/helm/README.md +16 -8
- package/deploy/helm/emilia-gate/Chart.yaml +3 -1
- package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
- package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
- package/deploy/helm/emilia-gate/values.yaml +18 -2
- package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
- package/deploy/helm/emilia-gate-service/README.md +81 -0
- package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
- package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
- package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
- package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
- package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
- package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
- package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
- package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
- package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
- package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
- package/deploy/helm/emilia-gate-service/values.yaml +166 -0
- package/deploy/sql/001-runtime.sql +707 -0
- package/deploy/terraform/README.md +24 -14
- package/deploy/terraform/main.tf +59 -0
- package/deploy/terraform/service/README.md +81 -0
- package/deploy/terraform/service/main.tf +718 -0
- package/deploy/terraform/service/outputs.tf +19 -0
- package/deploy/terraform/service/tests/validate.sh +24 -0
- package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
- package/deploy/terraform/service/variables.tf +459 -0
- package/deploy/terraform/service/versions.tf +10 -0
- package/deploy/terraform/variables.tf +95 -2
- package/deployment-attestation.js +248 -0
- package/eg1-conformance.js +46 -12
- package/enterprise.js +6 -2
- package/ep-assure.mjs +3 -2
- package/evidence-postgres.js +357 -0
- package/evidence.js +10 -3
- package/execution-binding.js +141 -26
- package/index.js +556 -115
- package/key-registry.js +51 -19
- package/mcp.js +4 -2
- package/network-witness.js +500 -0
- package/package.json +25 -5
- package/reliance-kernel.js +5 -6
- package/reliance-packet.js +43 -10
- package/reports/assurance-package.js +45 -19
- package/reports/reperform.js +78 -18
- package/reports/underwriter.js +1 -1
- package/settlement.js +300 -0
- package/store-postgres.js +14 -0
- package/store.js +26 -5
- package/strict-json.js +86 -0
- package/witness-postgres.js +97 -0
package/control-plane.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Vendor-neutral orchestration for Gate coverage, evidence completeness, and
|
|
4
|
+
* usage metering. Enforcement still happens at each executor-side Gate. This
|
|
5
|
+
* module cannot turn an observation or dashboard decision into authorization.
|
|
6
|
+
*/
|
|
7
|
+
import { canonicalize, hashCanonical } from './execution-binding.js';
|
|
8
|
+
import { coverageInventoryDigest, evaluateGateCoverage } from './coverage.js';
|
|
9
|
+
import { evaluateSettlementEligibility } from './settlement.js';
|
|
10
|
+
import { meterUsage, buildUsageStatement } from './metering.js';
|
|
11
|
+
import { acceptNetworkWitnessStatement, networkWitnessDigest } from './network-witness.js';
|
|
12
|
+
|
|
13
|
+
export const CONTROL_PLANE_REPORT_VERSION = 'EP-GATE-CONTROL-PLANE-REPORT-v1';
|
|
14
|
+
export const CONTROL_PLANE_MAX_SETTLEMENTS = 10_000;
|
|
15
|
+
const MAX_COVERAGE_EVIDENCE_ITEMS = 50_000;
|
|
16
|
+
|
|
17
|
+
function isPlainObject(value) {
|
|
18
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
19
|
+
const prototype = Object.getPrototypeOf(value);
|
|
20
|
+
return prototype === Object.prototype || prototype === null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function digest(value) {
|
|
24
|
+
return `sha256:${hashCanonical(value)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function witnessArtifactKey(statement) {
|
|
28
|
+
try { return digest(statement); } catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function immutableCanonicalSnapshot(value) {
|
|
32
|
+
const snapshot = JSON.parse(canonicalize(value));
|
|
33
|
+
const stack = [snapshot];
|
|
34
|
+
while (stack.length) {
|
|
35
|
+
const current = stack.pop();
|
|
36
|
+
if (current && typeof current === 'object') {
|
|
37
|
+
for (const child of Object.values(current)) stack.push(child);
|
|
38
|
+
Object.freeze(current);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return snapshot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function snapshotExpectedProbeNonces(value, inventory) {
|
|
45
|
+
if (value === undefined || value === null) return null;
|
|
46
|
+
if (value instanceof Map) {
|
|
47
|
+
const selected = Object.create(null);
|
|
48
|
+
for (const surface of Array.isArray(inventory?.surfaces) ? inventory.surfaces : []) {
|
|
49
|
+
if (value.has(surface.surface_id)) selected[surface.surface_id] = value.get(surface.surface_id);
|
|
50
|
+
}
|
|
51
|
+
return immutableCanonicalSnapshot(selected);
|
|
52
|
+
}
|
|
53
|
+
return immutableCanonicalSnapshot(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function snapshotSettlementItems(items) {
|
|
57
|
+
const ownKeys = Reflect.ownKeys(items);
|
|
58
|
+
if (ownKeys.length !== items.length + 1 || !ownKeys.includes('length')) {
|
|
59
|
+
throw new TypeError('settlement collection must be a dense data array');
|
|
60
|
+
}
|
|
61
|
+
const snapshots = [];
|
|
62
|
+
for (let i = 0; i < items.length; i++) {
|
|
63
|
+
const descriptor = Object.getOwnPropertyDescriptor(items, String(i));
|
|
64
|
+
if (!descriptor || descriptor.enumerable !== true || !Object.hasOwn(descriptor, 'value')) {
|
|
65
|
+
throw new TypeError('settlement collection contains an accessor');
|
|
66
|
+
}
|
|
67
|
+
snapshots.push(immutableCanonicalSnapshot(descriptor.value));
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze(snapshots);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Produce one reproducible control-plane view. The signed/verified subartifacts
|
|
74
|
+
* remain independently portable; this report joins only their digests and
|
|
75
|
+
* closed verdicts.
|
|
76
|
+
*/
|
|
77
|
+
export async function evaluateGateControlPlane(input = {}, options = {}) {
|
|
78
|
+
const now = options.now === undefined ? Date.now() : Number(options.now);
|
|
79
|
+
if (!Number.isFinite(now)) throw new TypeError('control-plane now must be finite');
|
|
80
|
+
|
|
81
|
+
let coverageInventory;
|
|
82
|
+
let settlementProfile;
|
|
83
|
+
let pinnedProbes;
|
|
84
|
+
let pinnedWitnesses;
|
|
85
|
+
let expectedProbeNonces;
|
|
86
|
+
let probeMaxAgeSec;
|
|
87
|
+
let witnessMaxAgeSec;
|
|
88
|
+
let maxFutureSkewSec;
|
|
89
|
+
let allowEphemeralWitnessStore;
|
|
90
|
+
let configuredTrustedAcceptances;
|
|
91
|
+
let configurationError = null;
|
|
92
|
+
try {
|
|
93
|
+
coverageInventory = options.coverageInventory === undefined
|
|
94
|
+
? undefined
|
|
95
|
+
: immutableCanonicalSnapshot(options.coverageInventory);
|
|
96
|
+
settlementProfile = options.settlementProfile === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: immutableCanonicalSnapshot(options.settlementProfile);
|
|
99
|
+
pinnedProbes = options.pinnedProbes === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: immutableCanonicalSnapshot(options.pinnedProbes);
|
|
102
|
+
pinnedWitnesses = options.pinnedWitnesses === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: immutableCanonicalSnapshot(options.pinnedWitnesses);
|
|
105
|
+
expectedProbeNonces = snapshotExpectedProbeNonces(options.expectedProbeNonces, coverageInventory);
|
|
106
|
+
probeMaxAgeSec = options.probeMaxAgeSec;
|
|
107
|
+
witnessMaxAgeSec = options.witnessMaxAgeSec;
|
|
108
|
+
maxFutureSkewSec = options.maxFutureSkewSec;
|
|
109
|
+
allowEphemeralWitnessStore = options.allowEphemeralWitnessStore === true;
|
|
110
|
+
const trustedInput = options.trustedWitnessAcceptances;
|
|
111
|
+
if (trustedInput !== undefined
|
|
112
|
+
&& (!Array.isArray(trustedInput) || trustedInput.length > MAX_COVERAGE_EVIDENCE_ITEMS)) {
|
|
113
|
+
throw new TypeError('trusted witness acceptance collection is invalid');
|
|
114
|
+
}
|
|
115
|
+
configuredTrustedAcceptances = [];
|
|
116
|
+
for (const acceptance of Array.isArray(trustedInput) ? trustedInput : []) {
|
|
117
|
+
configuredTrustedAcceptances.push(immutableCanonicalSnapshot(acceptance));
|
|
118
|
+
}
|
|
119
|
+
Object.freeze(configuredTrustedAcceptances);
|
|
120
|
+
} catch {
|
|
121
|
+
configurationError = 'rp_configuration_invalid';
|
|
122
|
+
coverageInventory = null;
|
|
123
|
+
settlementProfile = null;
|
|
124
|
+
pinnedProbes = [];
|
|
125
|
+
pinnedWitnesses = [];
|
|
126
|
+
expectedProbeNonces = null;
|
|
127
|
+
configuredTrustedAcceptances = Object.freeze([]);
|
|
128
|
+
allowEphemeralWitnessStore = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let attestationVerifiers;
|
|
132
|
+
let witnessSequenceStore;
|
|
133
|
+
let verifyAuthorization;
|
|
134
|
+
let verifyExecution;
|
|
135
|
+
let verifyOutcome;
|
|
136
|
+
try {
|
|
137
|
+
attestationVerifiers = options.attestationVerifiers;
|
|
138
|
+
witnessSequenceStore = options.witnessSequenceStore;
|
|
139
|
+
verifyAuthorization = options.verifyAuthorization;
|
|
140
|
+
verifyExecution = options.verifyExecution;
|
|
141
|
+
verifyOutcome = options.verifyOutcome;
|
|
142
|
+
} catch {
|
|
143
|
+
configurationError = 'rp_configuration_invalid';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let settlementItems = [];
|
|
147
|
+
let settlementError = null;
|
|
148
|
+
try {
|
|
149
|
+
const presentedSettlements = input.settlements;
|
|
150
|
+
if (presentedSettlements !== undefined) {
|
|
151
|
+
if (!Array.isArray(presentedSettlements)) {
|
|
152
|
+
settlementError = 'settlements_invalid';
|
|
153
|
+
} else if (presentedSettlements.length > CONTROL_PLANE_MAX_SETTLEMENTS) {
|
|
154
|
+
settlementError = 'settlements_limit_exceeded';
|
|
155
|
+
} else {
|
|
156
|
+
settlementItems = snapshotSettlementItems(presentedSettlements);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
settlementItems = [];
|
|
161
|
+
settlementError = 'settlements_hostile_input';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let coverageInput = {};
|
|
165
|
+
try {
|
|
166
|
+
const presentedCoverage = input.coverage;
|
|
167
|
+
if (isPlainObject(presentedCoverage)) coverageInput = presentedCoverage;
|
|
168
|
+
} catch { /* coverage will remain closed with no evidence */ }
|
|
169
|
+
|
|
170
|
+
let usageInput;
|
|
171
|
+
let usageError = null;
|
|
172
|
+
try {
|
|
173
|
+
const presentedUsage = input.usage;
|
|
174
|
+
usageInput = presentedUsage === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: immutableCanonicalSnapshot(presentedUsage);
|
|
177
|
+
} catch { usageError = 'usage_statement_refused'; }
|
|
178
|
+
|
|
179
|
+
// Evidence is presenter-controlled; policy and pins above are immutable RP
|
|
180
|
+
// snapshots. No bundle can select a weaker configuration while work awaits.
|
|
181
|
+
const trustedByStatementDigest = new Map();
|
|
182
|
+
for (const acceptance of configuredTrustedAcceptances) {
|
|
183
|
+
if (typeof acceptance.statement_digest === 'string') {
|
|
184
|
+
trustedByStatementDigest.set(acceptance.statement_digest, acceptance);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const coverageAcceptanceByArtifact = new Map();
|
|
188
|
+
const acceptanceForStatement = (statement) => {
|
|
189
|
+
const artifactKey = witnessArtifactKey(statement);
|
|
190
|
+
if (artifactKey && coverageAcceptanceByArtifact.has(artifactKey)) {
|
|
191
|
+
return { found: true, acceptance: coverageAcceptanceByArtifact.get(artifactKey), artifactKey };
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const statementDigest = networkWitnessDigest(statement);
|
|
195
|
+
if (trustedByStatementDigest.has(statementDigest)) {
|
|
196
|
+
return { found: true, acceptance: trustedByStatementDigest.get(statementDigest), artifactKey };
|
|
197
|
+
}
|
|
198
|
+
} catch { /* raw ingestion will refuse malformed statements */ }
|
|
199
|
+
return { found: false, acceptance: null, artifactKey };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Coverage and settlement may reference the same witness statement. Consume
|
|
203
|
+
// each coverage statement once, then reuse only this RP-side ingestion result.
|
|
204
|
+
let coverageWitnesses = null;
|
|
205
|
+
try {
|
|
206
|
+
coverageInventoryDigest(coverageInventory);
|
|
207
|
+
const evidenceListsValid = ['deployments', 'probes', 'witnesses'].every((field) => {
|
|
208
|
+
const value = coverageInput[field];
|
|
209
|
+
return value === undefined || (Array.isArray(value) && value.length <= MAX_COVERAGE_EVIDENCE_ITEMS);
|
|
210
|
+
});
|
|
211
|
+
if (evidenceListsValid) {
|
|
212
|
+
const presentedWitnesses = Array.isArray(coverageInput.witnesses) ? coverageInput.witnesses : [];
|
|
213
|
+
if (presentedWitnesses.length + configuredTrustedAcceptances.length <= MAX_COVERAGE_EVIDENCE_ITEMS) {
|
|
214
|
+
coverageWitnesses = presentedWitnesses.map((statement) => immutableCanonicalSnapshot(statement));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch { /* coverage owns the closed invalid-input report */ }
|
|
218
|
+
|
|
219
|
+
const coverageIngestionResults = [];
|
|
220
|
+
if (coverageWitnesses) {
|
|
221
|
+
for (const statement of coverageWitnesses) {
|
|
222
|
+
const existing = acceptanceForStatement(statement);
|
|
223
|
+
const acceptance = existing.found
|
|
224
|
+
? existing.acceptance
|
|
225
|
+
: await acceptNetworkWitnessStatement(statement, {
|
|
226
|
+
pinnedWitnesses,
|
|
227
|
+
maxAgeSec: witnessMaxAgeSec,
|
|
228
|
+
maxFutureSkewSec,
|
|
229
|
+
now,
|
|
230
|
+
sequenceStore: witnessSequenceStore,
|
|
231
|
+
allowEphemeralStore: allowEphemeralWitnessStore,
|
|
232
|
+
});
|
|
233
|
+
if (existing.artifactKey) coverageAcceptanceByArtifact.set(existing.artifactKey, acceptance);
|
|
234
|
+
coverageIngestionResults.push(acceptance);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const coverageEvidence = coverageWitnesses
|
|
238
|
+
? { ...coverageInput, witnesses: [] }
|
|
239
|
+
: coverageInput;
|
|
240
|
+
const coverage = await evaluateGateCoverage({
|
|
241
|
+
...coverageEvidence,
|
|
242
|
+
inventory: coverageInventory,
|
|
243
|
+
}, {
|
|
244
|
+
now,
|
|
245
|
+
attestationVerifiers,
|
|
246
|
+
pinnedProbes,
|
|
247
|
+
pinnedWitnesses,
|
|
248
|
+
expectedProbeNonces,
|
|
249
|
+
probeMaxAgeSec,
|
|
250
|
+
witnessMaxAgeSec,
|
|
251
|
+
maxFutureSkewSec,
|
|
252
|
+
witnessSequenceStore,
|
|
253
|
+
allowEphemeralWitnessStore,
|
|
254
|
+
trustedWitnessAcceptances: [
|
|
255
|
+
...configuredTrustedAcceptances,
|
|
256
|
+
...coverageIngestionResults,
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const settlements = [];
|
|
261
|
+
for (const item of settlementItems) {
|
|
262
|
+
const bundle = isPlainObject(item?.bundle) ? item.bundle : {};
|
|
263
|
+
const surfaceId = bundle?.coverage?.surface_id;
|
|
264
|
+
const verifyCoverage = async () => {
|
|
265
|
+
const row = Array.isArray(coverage.surfaces)
|
|
266
|
+
? coverage.surfaces.find((candidate) => candidate.surface_id === surfaceId)
|
|
267
|
+
: null;
|
|
268
|
+
if (!row) return { accepted: false, reason: 'surface_not_in_pinned_inventory' };
|
|
269
|
+
return {
|
|
270
|
+
accepted: row.complete === true,
|
|
271
|
+
state: row.state,
|
|
272
|
+
surface_id: row.surface_id,
|
|
273
|
+
report_hash: coverage.report_hash,
|
|
274
|
+
reason: row.complete ? null : row.reason,
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
const settlementOptions = {
|
|
278
|
+
profile: settlementProfile,
|
|
279
|
+
now,
|
|
280
|
+
pinnedWitnesses,
|
|
281
|
+
witnessMaxAgeSec,
|
|
282
|
+
maxFutureSkewSec,
|
|
283
|
+
witnessSequenceStore,
|
|
284
|
+
allowEphemeralWitnessStore,
|
|
285
|
+
verifyAuthorization,
|
|
286
|
+
verifyExecution,
|
|
287
|
+
verifyOutcome,
|
|
288
|
+
verifyCoverage,
|
|
289
|
+
};
|
|
290
|
+
if (settlementProfile?.require_witness === true) {
|
|
291
|
+
const trusted = acceptanceForStatement(bundle.witness);
|
|
292
|
+
if (trusted.found) settlementOptions.trustedWitnessAcceptance = trusted.acceptance;
|
|
293
|
+
}
|
|
294
|
+
settlements.push(await evaluateSettlementEligibility(bundle, settlementOptions));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let usage = null;
|
|
298
|
+
if (usageInput !== undefined && !usageError) {
|
|
299
|
+
try {
|
|
300
|
+
const metered = meterUsage(usageInput?.entries ?? [], usageInput?.period ?? {});
|
|
301
|
+
usage = buildUsageStatement(metered, { org: usageInput?.org });
|
|
302
|
+
} catch {
|
|
303
|
+
usageError = 'usage_statement_refused';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const summary = {
|
|
308
|
+
'@version': CONTROL_PLANE_REPORT_VERSION,
|
|
309
|
+
generated_at: new Date(now).toISOString(),
|
|
310
|
+
coverage_report_hash: coverage.report_hash ?? null,
|
|
311
|
+
coverage_complete: coverage.complete === true,
|
|
312
|
+
settlement_input_complete: settlementError === null,
|
|
313
|
+
settlement_results: settlements.map((result) => ({
|
|
314
|
+
action_digest: result.action_digest,
|
|
315
|
+
verdict: result.verdict,
|
|
316
|
+
eligible: result.eligible === true,
|
|
317
|
+
result_hash: result.result_hash,
|
|
318
|
+
})),
|
|
319
|
+
usage_statement_hash: usage?.content_hash ? `sha256:${usage.content_hash}` : null,
|
|
320
|
+
usage_complete: usage?.complete === true,
|
|
321
|
+
...(configurationError ? { configuration_error: configurationError } : {}),
|
|
322
|
+
...(settlementError ? { settlement_error: settlementError } : {}),
|
|
323
|
+
...(usageError ? { usage_error: usageError } : {}),
|
|
324
|
+
limitations: [
|
|
325
|
+
'The control plane coordinates policy, evidence, coverage, metering, and settlement eligibility; executor-side Gates remain the only enforcement points.',
|
|
326
|
+
'Coverage is bounded by the relying-party-declared inventory, and a passive witness never upgrades a surface to gated.',
|
|
327
|
+
'Witness-dependent decisions require durable sequence ingestion or an explicitly relying-party-trusted acceptance result.',
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
return Object.freeze({
|
|
331
|
+
...summary,
|
|
332
|
+
control_plane_digest: digest(summary),
|
|
333
|
+
artifacts: Object.freeze({ coverage, settlements, usage }),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export default {
|
|
338
|
+
CONTROL_PLANE_REPORT_VERSION,
|
|
339
|
+
CONTROL_PLANE_MAX_SETTLEMENTS,
|
|
340
|
+
evaluateGateControlPlane,
|
|
341
|
+
};
|