@simplexable/kc2-client 0.3.0 → 0.5.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplexable/kc2-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Host-neutral KC2 contracts for platform integrations.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
"./reconciliation/kc2ReconciliationStageContracts": "./src/reconciliation/kc2ReconciliationStageContracts.js",
|
|
21
21
|
"./reconciliation/kc2ReconciliationContract": "./src/reconciliation/kc2ReconciliationContract.js",
|
|
22
22
|
"./reconciliation/stageArtifactEnvelope": "./src/reconciliation/stageArtifactEnvelope.js",
|
|
23
|
+
"./authority/authorityDecisionContract": "./src/authority/authorityDecisionContract.js",
|
|
23
24
|
"./authority/authorityContracts": "./src/authority/authorityContracts.js",
|
|
24
25
|
"./authority/humanGuidanceContracts": "./src/authority/humanGuidanceContracts.js",
|
|
25
26
|
"./authority/authorityDispositionContracts": "./src/authority/authorityDispositionContracts.js",
|
|
27
|
+
"./protectedContent/protectedContentReadinessContract": "./src/protectedContent/protectedContentReadinessContract.js",
|
|
26
28
|
"./persistence/kc2WireOperationContracts": "./src/persistence/kc2WireOperationContracts.js",
|
|
27
29
|
"./persistence/kc2GenerationSnapshotPersistenceContract": "./src/persistence/kc2GenerationSnapshotPersistenceContract.js",
|
|
28
30
|
"./persistence/kc2HumanAuthorityReadContract": "./src/persistence/kc2HumanAuthorityReadContract.js",
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
AUTHORITY_DISPOSITION,
|
|
5
|
+
AUTHORITY_OVERALL_OUTCOME,
|
|
6
|
+
} = require('./authorityContracts');
|
|
7
|
+
const { AUTHORITY_CONFLICT_KIND } = require('./authorityDispositionContracts');
|
|
8
|
+
|
|
9
|
+
const CONTRACT_VERSION = 'authority-decision.v1';
|
|
10
|
+
const DISPOSITION_REPORT_VERSION = 'kc2-human-authority-disposition.v1';
|
|
11
|
+
const OPERATION = 'authority-decision';
|
|
12
|
+
const MAX_STRING_LENGTH = 512;
|
|
13
|
+
const MAX_ARRAY_LENGTH = 64;
|
|
14
|
+
const MAX_VIOLATIONS = 64;
|
|
15
|
+
const MAX_REASONS = 16;
|
|
16
|
+
|
|
17
|
+
const ERROR_CATEGORIES = Object.freeze([
|
|
18
|
+
'invalid-payload',
|
|
19
|
+
'unauthorized',
|
|
20
|
+
'unavailable',
|
|
21
|
+
'unsupported-operation',
|
|
22
|
+
'semantic-failure',
|
|
23
|
+
]);
|
|
24
|
+
const ERROR_CODES = Object.freeze([
|
|
25
|
+
'invalid-payload',
|
|
26
|
+
'unauthorized',
|
|
27
|
+
'provider-unavailable',
|
|
28
|
+
'unsupported-operation',
|
|
29
|
+
'semantic-failure',
|
|
30
|
+
]);
|
|
31
|
+
const CONFLICT_KINDS = Object.freeze(Object.values(AUTHORITY_CONFLICT_KIND));
|
|
32
|
+
const POTENTIAL_CONFLICT_KIND = 'potential-human-authority-conflict';
|
|
33
|
+
const POTENTIAL_CONFLICT_KINDS = Object.freeze([...CONFLICT_KINDS, POTENTIAL_CONFLICT_KIND]);
|
|
34
|
+
const CURRENTNESS_REASON_CODES = Object.freeze([
|
|
35
|
+
'authority-context-fingerprint-changed',
|
|
36
|
+
'evidence-fingerprint-changed',
|
|
37
|
+
'policy-version-changed',
|
|
38
|
+
'authority-fingerprint-unavailable',
|
|
39
|
+
]);
|
|
40
|
+
const FINGERPRINT_KEYS = Object.freeze(['authorityContext', 'evidence', 'policyVersion']);
|
|
41
|
+
const ACTIVE_RECORD_SECTIONS = Object.freeze([
|
|
42
|
+
'domainAssertions',
|
|
43
|
+
'authoringGuidance',
|
|
44
|
+
'addenda',
|
|
45
|
+
'protectedOverrides',
|
|
46
|
+
]);
|
|
47
|
+
const DISPOSITION_ENTRY_ID_FIELDS = Object.freeze({
|
|
48
|
+
addenda: 'addendumId',
|
|
49
|
+
authoringGuidance: 'guidanceId',
|
|
50
|
+
domainAssertions: 'assertionId',
|
|
51
|
+
protectedOverrides: 'protectionId',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function contractError(message) {
|
|
55
|
+
const error = new TypeError(message);
|
|
56
|
+
error.code = 'invalid-payload';
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertPlainObject(value, label) {
|
|
61
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
62
|
+
throw contractError(`${label} must be an object`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertNoUnknownKeys(value, allowed, label) {
|
|
67
|
+
for (const key of Object.keys(value)) {
|
|
68
|
+
if (!allowed.includes(key)) throw contractError(`${label}.${key} is not supported`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertString(value, label, { nullable = false } = {}) {
|
|
73
|
+
if (nullable && value === null) return;
|
|
74
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_STRING_LENGTH) {
|
|
75
|
+
throw contractError(`${label} must be a bounded non-empty string${nullable ? ' or null' : ''}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertNullableString(value, label) {
|
|
80
|
+
assertString(value, label, { nullable: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertNullableBoolean(value, label) {
|
|
84
|
+
if (value !== null && typeof value !== 'boolean') throw contractError(`${label} must be boolean or null`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertBoolean(value, label) {
|
|
88
|
+
if (typeof value !== 'boolean') throw contractError(`${label} must be boolean`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertEnum(value, values, label, { nullable = false } = {}) {
|
|
92
|
+
if (nullable && value === null) return;
|
|
93
|
+
if (typeof value !== 'string' || !values.includes(value)) {
|
|
94
|
+
throw contractError(`${label} is not a supported value`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertBoundedArray(value, label) {
|
|
99
|
+
if (!Array.isArray(value) || value.length > MAX_ARRAY_LENGTH) {
|
|
100
|
+
throw contractError(`${label} must be a bounded array`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertFingerprintMap(value, label) {
|
|
105
|
+
assertPlainObject(value, label);
|
|
106
|
+
assertNoUnknownKeys(value, [...FINGERPRINT_KEYS], label);
|
|
107
|
+
for (const key of FINGERPRINT_KEYS) {
|
|
108
|
+
if (Object.hasOwn(value, key)) assertNullableString(value[key], `${label}.${key}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateDispositionEntry(entry, idField, label) {
|
|
113
|
+
assertPlainObject(entry, label);
|
|
114
|
+
assertNoUnknownKeys(entry, [idField, 'disposition', 'incorporation', 'rationale'], label);
|
|
115
|
+
assertString(entry[idField], `${label}.${idField}`);
|
|
116
|
+
assertEnum(entry.disposition, AUTHORITY_DISPOSITION, `${label}.disposition`);
|
|
117
|
+
if (Object.hasOwn(entry, 'incorporation') && entry.incorporation !== null) {
|
|
118
|
+
assertPlainObject(entry.incorporation, `${label}.incorporation`);
|
|
119
|
+
assertNoUnknownKeys(entry.incorporation, ['articleLayer', 'passageIndex'], `${label}.incorporation`);
|
|
120
|
+
}
|
|
121
|
+
if (Object.hasOwn(entry, 'rationale') && entry.rationale !== null) {
|
|
122
|
+
assertString(entry.rationale, `${label}.rationale`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateDispositionReport(report, label) {
|
|
127
|
+
assertPlainObject(report, label);
|
|
128
|
+
assertNoUnknownKeys(report, [
|
|
129
|
+
'articleKey',
|
|
130
|
+
'conflicts',
|
|
131
|
+
'contractVersion',
|
|
132
|
+
'domainAssertions',
|
|
133
|
+
'authoringGuidance',
|
|
134
|
+
'addenda',
|
|
135
|
+
'protectedOverrides',
|
|
136
|
+
'overallOutcome',
|
|
137
|
+
'policy',
|
|
138
|
+
], label);
|
|
139
|
+
if (report.contractVersion !== DISPOSITION_REPORT_VERSION) {
|
|
140
|
+
throw contractError(`${label}.contractVersion is unsupported`);
|
|
141
|
+
}
|
|
142
|
+
if (Object.hasOwn(report, 'overallOutcome')) {
|
|
143
|
+
assertEnum(report.overallOutcome, AUTHORITY_OVERALL_OUTCOME, `${label}.overallOutcome`);
|
|
144
|
+
}
|
|
145
|
+
for (const section of ACTIVE_RECORD_SECTIONS) {
|
|
146
|
+
if (Object.hasOwn(report, section)) {
|
|
147
|
+
assertBoundedArray(report[section], `${label}.${section}`);
|
|
148
|
+
report[section].forEach((entry, index) => {
|
|
149
|
+
validateDispositionEntry(entry, DISPOSITION_ENTRY_ID_FIELDS[section], `${label}.${section}[${index}]`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (Object.hasOwn(report, 'conflicts')) {
|
|
154
|
+
assertBoundedArray(report.conflicts, `${label}.conflicts`);
|
|
155
|
+
report.conflicts.forEach((conflict, index) => {
|
|
156
|
+
validateConflictRecord(conflict, `${label}.conflicts[${index}]`);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateConflictRecord(conflict, label, { potential = false } = {}) {
|
|
162
|
+
assertPlainObject(conflict, label);
|
|
163
|
+
assertNoUnknownKeys(conflict, [
|
|
164
|
+
'authorities',
|
|
165
|
+
'kind',
|
|
166
|
+
'scope',
|
|
167
|
+
'reason',
|
|
168
|
+
'targetIdentity',
|
|
169
|
+
'subjectKey',
|
|
170
|
+
'guidanceId',
|
|
171
|
+
'guidanceTypes',
|
|
172
|
+
'assertionTextHashes',
|
|
173
|
+
'normalizedScopeIdentity',
|
|
174
|
+
'scopeKey',
|
|
175
|
+
'narrationMatches',
|
|
176
|
+
], label);
|
|
177
|
+
if (Object.hasOwn(conflict, 'kind')) {
|
|
178
|
+
assertEnum(
|
|
179
|
+
conflict.kind,
|
|
180
|
+
potential ? POTENTIAL_CONFLICT_KINDS : CONFLICT_KINDS,
|
|
181
|
+
`${label}.kind`,
|
|
182
|
+
{ nullable: true },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (Object.hasOwn(conflict, 'authorities')) {
|
|
186
|
+
assertBoundedArray(conflict.authorities, `${label}.authorities`);
|
|
187
|
+
conflict.authorities.forEach((entry, index) => {
|
|
188
|
+
assertString(entry, `${label}.authorities[${index}]`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (Object.hasOwn(conflict, 'scope') && conflict.scope !== null) {
|
|
192
|
+
assertPlainObject(conflict.scope, `${label}.scope`);
|
|
193
|
+
assertNoUnknownKeys(conflict.scope, ['articleKey', 'layer'], `${label}.scope`);
|
|
194
|
+
if (Object.hasOwn(conflict.scope, 'articleKey')) {
|
|
195
|
+
assertString(conflict.scope.articleKey, `${label}.scope.articleKey`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (Object.hasOwn(conflict, 'reason')) assertNullableString(conflict.reason, `${label}.reason`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateAuthorityConflicts(value, label) {
|
|
202
|
+
assertPlainObject(value, label);
|
|
203
|
+
assertNoUnknownKeys(value, ['conflicts', 'potentialHumanAuthorityConflicts'], label);
|
|
204
|
+
for (const key of ['conflicts', 'potentialHumanAuthorityConflicts']) {
|
|
205
|
+
assertBoundedArray(value[key], `${label}.${key}`);
|
|
206
|
+
value[key].forEach((entry, index) => validateConflictRecord(
|
|
207
|
+
entry,
|
|
208
|
+
`${label}.${key}[${index}]`,
|
|
209
|
+
{ potential: key === 'potentialHumanAuthorityConflicts' },
|
|
210
|
+
));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validateAuthorityCurrentness(value, label) {
|
|
215
|
+
if (value === null) return;
|
|
216
|
+
assertPlainObject(value, label);
|
|
217
|
+
assertNoUnknownKeys(value, ['current', 'stale', 'unavailable', 'reasons'], label);
|
|
218
|
+
if (Object.hasOwn(value, 'current')) assertNullableBoolean(value.current, `${label}.current`);
|
|
219
|
+
if (Object.hasOwn(value, 'stale')) assertNullableBoolean(value.stale, `${label}.stale`);
|
|
220
|
+
if (Object.hasOwn(value, 'unavailable')) assertBoolean(value.unavailable, `${label}.unavailable`);
|
|
221
|
+
if (Object.hasOwn(value, 'reasons')) {
|
|
222
|
+
assertBoundedArray(value.reasons, `${label}.reasons`);
|
|
223
|
+
value.reasons.forEach((reason, index) => {
|
|
224
|
+
assertEnum(reason, CURRENTNESS_REASON_CODES, `${label}.reasons[${index}]`, { nullable: true });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function validateAuthorityDecisionRequest(input) {
|
|
230
|
+
assertPlainObject(input, 'request');
|
|
231
|
+
assertNoUnknownKeys(input, [
|
|
232
|
+
'contractVersion',
|
|
233
|
+
'platformId',
|
|
234
|
+
'articleKey',
|
|
235
|
+
'authorityPresent',
|
|
236
|
+
'domainAssertions',
|
|
237
|
+
'authoringGuidance',
|
|
238
|
+
'guidancePolicyFindings',
|
|
239
|
+
'policy',
|
|
240
|
+
'dispositionReport',
|
|
241
|
+
'articleStageReport',
|
|
242
|
+
'decisionStageReport',
|
|
243
|
+
'activeRecords',
|
|
244
|
+
'deterministicProtectedConflicts',
|
|
245
|
+
'currentAuthorityFingerprints',
|
|
246
|
+
'persistedAuthorityFingerprints',
|
|
247
|
+
'contextArticleKey',
|
|
248
|
+
'contextPolicyVersion',
|
|
249
|
+
'requestId',
|
|
250
|
+
], 'request');
|
|
251
|
+
if (input.contractVersion !== CONTRACT_VERSION) throw contractError('request.contractVersion is unsupported');
|
|
252
|
+
assertString(input.platformId, 'request.platformId');
|
|
253
|
+
assertString(input.articleKey, 'request.articleKey');
|
|
254
|
+
if (Object.hasOwn(input, 'authorityPresent')) assertBoolean(input.authorityPresent, 'request.authorityPresent');
|
|
255
|
+
if (Object.hasOwn(input, 'domainAssertions')) {
|
|
256
|
+
assertBoundedArray(input.domainAssertions, 'request.domainAssertions');
|
|
257
|
+
}
|
|
258
|
+
if (Object.hasOwn(input, 'authoringGuidance')) {
|
|
259
|
+
assertBoundedArray(input.authoringGuidance, 'request.authoringGuidance');
|
|
260
|
+
}
|
|
261
|
+
if (Object.hasOwn(input, 'guidancePolicyFindings')) {
|
|
262
|
+
assertBoundedArray(input.guidancePolicyFindings, 'request.guidancePolicyFindings');
|
|
263
|
+
}
|
|
264
|
+
if (Object.hasOwn(input, 'policy') && input.policy !== null) assertPlainObject(input.policy, 'request.policy');
|
|
265
|
+
if (Object.hasOwn(input, 'dispositionReport') && input.dispositionReport !== null) {
|
|
266
|
+
validateDispositionReport(input.dispositionReport, 'request.dispositionReport');
|
|
267
|
+
}
|
|
268
|
+
if (Object.hasOwn(input, 'articleStageReport') && input.articleStageReport !== null) {
|
|
269
|
+
validateDispositionReport(input.articleStageReport, 'request.articleStageReport');
|
|
270
|
+
}
|
|
271
|
+
if (Object.hasOwn(input, 'decisionStageReport') && input.decisionStageReport !== null) {
|
|
272
|
+
validateDispositionReport(input.decisionStageReport, 'request.decisionStageReport');
|
|
273
|
+
}
|
|
274
|
+
if (Object.hasOwn(input, 'activeRecords') && input.activeRecords !== null) {
|
|
275
|
+
assertPlainObject(input.activeRecords, 'request.activeRecords');
|
|
276
|
+
assertNoUnknownKeys(input.activeRecords, ACTIVE_RECORD_SECTIONS, 'request.activeRecords');
|
|
277
|
+
}
|
|
278
|
+
if (Object.hasOwn(input, 'deterministicProtectedConflicts')) {
|
|
279
|
+
assertBoundedArray(input.deterministicProtectedConflicts, 'request.deterministicProtectedConflicts');
|
|
280
|
+
}
|
|
281
|
+
if (Object.hasOwn(input, 'currentAuthorityFingerprints') && input.currentAuthorityFingerprints !== null) {
|
|
282
|
+
assertFingerprintMap(input.currentAuthorityFingerprints, 'request.currentAuthorityFingerprints');
|
|
283
|
+
}
|
|
284
|
+
if (Object.hasOwn(input, 'persistedAuthorityFingerprints') && input.persistedAuthorityFingerprints !== null) {
|
|
285
|
+
assertFingerprintMap(input.persistedAuthorityFingerprints, 'request.persistedAuthorityFingerprints');
|
|
286
|
+
}
|
|
287
|
+
if (Object.hasOwn(input, 'contextArticleKey')) assertNullableString(input.contextArticleKey, 'request.contextArticleKey');
|
|
288
|
+
if (Object.hasOwn(input, 'contextPolicyVersion')) {
|
|
289
|
+
assertNullableString(input.contextPolicyVersion, 'request.contextPolicyVersion');
|
|
290
|
+
}
|
|
291
|
+
if (Object.hasOwn(input, 'requestId')) assertNullableString(input.requestId, 'request.requestId');
|
|
292
|
+
return input;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function validateAuthorityDecisionResponse(input) {
|
|
296
|
+
assertPlainObject(input, 'response');
|
|
297
|
+
assertNoUnknownKeys(input, [
|
|
298
|
+
'contractVersion',
|
|
299
|
+
'success',
|
|
300
|
+
'operation',
|
|
301
|
+
'provider',
|
|
302
|
+
'result',
|
|
303
|
+
'error',
|
|
304
|
+
], 'response');
|
|
305
|
+
if (input.contractVersion !== CONTRACT_VERSION) throw contractError('response.contractVersion is unsupported');
|
|
306
|
+
assertBoolean(input.success, 'response.success');
|
|
307
|
+
if (input.operation !== OPERATION) throw contractError('response.operation is unsupported');
|
|
308
|
+
assertPlainObject(input.provider, 'response.provider');
|
|
309
|
+
assertNoUnknownKeys(input.provider, ['providerId', 'providerVersion'], 'response.provider');
|
|
310
|
+
assertString(input.provider.providerId, 'response.provider.providerId');
|
|
311
|
+
assertString(input.provider.providerVersion, 'response.provider.providerVersion');
|
|
312
|
+
if (input.success) {
|
|
313
|
+
if (input.error !== null) throw contractError('response.error must be null on success');
|
|
314
|
+
assertPlainObject(input.result, 'response.result');
|
|
315
|
+
assertNoUnknownKeys(input.result, [
|
|
316
|
+
'authorityConflicts',
|
|
317
|
+
'authorityDisposition',
|
|
318
|
+
'validation',
|
|
319
|
+
'conflictsAddressed',
|
|
320
|
+
'authorityCurrentness',
|
|
321
|
+
], 'response.result');
|
|
322
|
+
validateAuthorityConflicts(input.result.authorityConflicts, 'response.result.authorityConflicts');
|
|
323
|
+
if (Object.hasOwn(input.result, 'authorityDisposition') && input.result.authorityDisposition !== null) {
|
|
324
|
+
assertPlainObject(input.result.authorityDisposition, 'response.result.authorityDisposition');
|
|
325
|
+
assertNoUnknownKeys(input.result.authorityDisposition, [
|
|
326
|
+
'contractVersion',
|
|
327
|
+
'articleKey',
|
|
328
|
+
'conflicts',
|
|
329
|
+
'domainAssertions',
|
|
330
|
+
'authoringGuidance',
|
|
331
|
+
'addenda',
|
|
332
|
+
'protectedOverrides',
|
|
333
|
+
'overallOutcome',
|
|
334
|
+
'policy',
|
|
335
|
+
'source',
|
|
336
|
+
], 'response.result.authorityDisposition');
|
|
337
|
+
if (Object.hasOwn(input.result.authorityDisposition, 'contractVersion')) {
|
|
338
|
+
assertEnum(
|
|
339
|
+
input.result.authorityDisposition.contractVersion,
|
|
340
|
+
[DISPOSITION_REPORT_VERSION],
|
|
341
|
+
'response.result.authorityDisposition.contractVersion',
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (Object.hasOwn(input.result.authorityDisposition, 'source')) {
|
|
345
|
+
assertNullableString(input.result.authorityDisposition.source, 'response.result.authorityDisposition.source');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
assertPlainObject(input.result.validation, 'response.result.validation');
|
|
349
|
+
assertNoUnknownKeys(input.result.validation, ['valid', 'applyEligible', 'violations'], 'response.result.validation');
|
|
350
|
+
assertBoolean(input.result.validation.valid, 'response.result.validation.valid');
|
|
351
|
+
assertBoolean(input.result.validation.applyEligible, 'response.result.validation.applyEligible');
|
|
352
|
+
assertBoundedArray(input.result.validation.violations, 'response.result.validation.violations');
|
|
353
|
+
if (input.result.validation.violations.length > MAX_VIOLATIONS) {
|
|
354
|
+
throw contractError('response.result.validation.violations must be a bounded array');
|
|
355
|
+
}
|
|
356
|
+
assertBoundedArray(input.result.conflictsAddressed, 'response.result.conflictsAddressed');
|
|
357
|
+
validateAuthorityCurrentness(input.result.authorityCurrentness, 'response.result.authorityCurrentness');
|
|
358
|
+
} else {
|
|
359
|
+
if (input.result !== null) throw contractError('response.result must be null on failure');
|
|
360
|
+
assertPlainObject(input.error, 'response.error');
|
|
361
|
+
assertNoUnknownKeys(input.error, ['code', 'category', 'message'], 'response.error');
|
|
362
|
+
for (const key of ['code', 'category', 'message']) {
|
|
363
|
+
if (!Object.hasOwn(input.error, key)) throw contractError(`response.error.${key} is required`);
|
|
364
|
+
}
|
|
365
|
+
assertEnum(input.error.code, ERROR_CODES, 'response.error.code');
|
|
366
|
+
assertEnum(input.error.category, ERROR_CATEGORIES, 'response.error.category');
|
|
367
|
+
assertString(input.error.message, 'response.error.message');
|
|
368
|
+
}
|
|
369
|
+
return input;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeAuthorityDecisionRequest(input) {
|
|
373
|
+
validateAuthorityDecisionRequest(input);
|
|
374
|
+
return JSON.parse(JSON.stringify(input));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function normalizeAuthorityDecisionResponse(input) {
|
|
378
|
+
validateAuthorityDecisionResponse(input);
|
|
379
|
+
return JSON.parse(JSON.stringify(input));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
ACTIVE_RECORD_SECTIONS,
|
|
384
|
+
AUTHORITY_DISPOSITION,
|
|
385
|
+
AUTHORITY_OVERALL_OUTCOME,
|
|
386
|
+
CONFLICT_KINDS,
|
|
387
|
+
POTENTIAL_CONFLICT_KIND,
|
|
388
|
+
POTENTIAL_CONFLICT_KINDS,
|
|
389
|
+
CONTRACT_VERSION,
|
|
390
|
+
CURRENTNESS_REASON_CODES,
|
|
391
|
+
DISPOSITION_REPORT_VERSION,
|
|
392
|
+
ERROR_CATEGORIES,
|
|
393
|
+
ERROR_CODES,
|
|
394
|
+
MAX_ARRAY_LENGTH,
|
|
395
|
+
MAX_STRING_LENGTH,
|
|
396
|
+
MAX_VIOLATIONS,
|
|
397
|
+
OPERATION,
|
|
398
|
+
normalizeAuthorityDecisionRequest,
|
|
399
|
+
normalizeAuthorityDecisionResponse,
|
|
400
|
+
validateAuthorityDecisionRequest,
|
|
401
|
+
validateAuthorityDecisionResponse,
|
|
402
|
+
};
|
package/src/index.js
CHANGED
|
@@ -24,10 +24,14 @@ module.exports = {
|
|
|
24
24
|
stageArtifactEnvelope: require('./reconciliation/stageArtifactEnvelope'),
|
|
25
25
|
},
|
|
26
26
|
authority: {
|
|
27
|
+
authorityDecisionContract: require('./authority/authorityDecisionContract'),
|
|
27
28
|
authorityContracts: require('./authority/authorityContracts'),
|
|
28
29
|
authorityDispositionContracts: require('./authority/authorityDispositionContracts'),
|
|
29
30
|
humanGuidanceContracts: require('./authority/humanGuidanceContracts'),
|
|
30
31
|
},
|
|
32
|
+
protectedContent: {
|
|
33
|
+
protectedContentReadinessContract: require('./protectedContent/protectedContentReadinessContract'),
|
|
34
|
+
},
|
|
31
35
|
persistence: {
|
|
32
36
|
kc2WireOperationContracts: require('./persistence/kc2WireOperationContracts'),
|
|
33
37
|
kc2GenerationSnapshotPersistenceContract: require('./persistence/kc2GenerationSnapshotPersistenceContract'),
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CONTRACT_VERSION = 'protected-content-readiness.v1';
|
|
4
|
+
const OPERATION = 'protected-content-readiness';
|
|
5
|
+
const MAX_STRING_LENGTH = 512;
|
|
6
|
+
const MAX_REASONS = 16;
|
|
7
|
+
|
|
8
|
+
const OUTCOMES = Object.freeze([
|
|
9
|
+
'ready-for-adoption',
|
|
10
|
+
'ready-for-human-decision',
|
|
11
|
+
'blocked-recovery',
|
|
12
|
+
'blocked-repository-changed',
|
|
13
|
+
'blocked-authority-context-changed',
|
|
14
|
+
'blocked-protection-changed',
|
|
15
|
+
'blocked-candidate-invalid',
|
|
16
|
+
'blocked-addendum-duplication',
|
|
17
|
+
'unavailable',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const BLOCKING_REASON_CODES = Object.freeze([
|
|
21
|
+
'inspection-artifact-unavailable',
|
|
22
|
+
'fingerprint-unavailable',
|
|
23
|
+
'authority-context-fingerprint-changed',
|
|
24
|
+
'repository-fingerprint-changed',
|
|
25
|
+
'candidate-fingerprint-changed',
|
|
26
|
+
'candidate-fingerprint-unavailable',
|
|
27
|
+
'candidate-facts-unavailable',
|
|
28
|
+
'human-authority-context-failed',
|
|
29
|
+
'human-authority-context-unavailable',
|
|
30
|
+
'protection-facts-unavailable',
|
|
31
|
+
'protection-not-current',
|
|
32
|
+
'protection-identity-changed',
|
|
33
|
+
'protection-not-active',
|
|
34
|
+
'addendum-facts-unavailable',
|
|
35
|
+
'addendum-duplication-observed',
|
|
36
|
+
'human-decision-pending',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const HUMAN_AUTHORITY_CONTEXT_OUTCOMES = Object.freeze([
|
|
40
|
+
'success',
|
|
41
|
+
'failed',
|
|
42
|
+
'not-applicable',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const LEGACY_REJECTED_REQUEST_KEYS = Object.freeze([
|
|
46
|
+
'blockingReasons',
|
|
47
|
+
'humanAuthorityContextStatus',
|
|
48
|
+
'humanAuthorityContextSuccess',
|
|
49
|
+
'dispositionOverallOutcome',
|
|
50
|
+
'authorityCurrent',
|
|
51
|
+
'repositoryCurrent',
|
|
52
|
+
'protectionCurrent',
|
|
53
|
+
'candidateValid',
|
|
54
|
+
'addendumValid',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const ERROR_CATEGORIES = Object.freeze([
|
|
58
|
+
'invalid-payload',
|
|
59
|
+
'unauthorized',
|
|
60
|
+
'unavailable',
|
|
61
|
+
'unsupported-operation',
|
|
62
|
+
'semantic-failure',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const ERROR_CODES = Object.freeze([
|
|
66
|
+
'invalid-payload',
|
|
67
|
+
'unauthorized',
|
|
68
|
+
'provider-unavailable',
|
|
69
|
+
'unsupported-operation',
|
|
70
|
+
'semantic-failure',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
function contractError(message) {
|
|
74
|
+
const error = new TypeError(message);
|
|
75
|
+
error.code = 'invalid-payload';
|
|
76
|
+
return error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertPlainObject(value, label) {
|
|
80
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
81
|
+
throw contractError(`${label} must be an object`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertNoUnknownKeys(value, allowed, label) {
|
|
86
|
+
for (const key of Object.keys(value)) {
|
|
87
|
+
if (!allowed.includes(key)) throw contractError(`${label}.${key} is not supported`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertString(value, label, { nullable = false } = {}) {
|
|
92
|
+
if (nullable && value === null) return;
|
|
93
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_STRING_LENGTH) {
|
|
94
|
+
throw contractError(`${label} must be a bounded non-empty string${nullable ? ' or null' : ''}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertBoolean(value, label) {
|
|
99
|
+
if (typeof value !== 'boolean') throw contractError(`${label} must be boolean`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertEnum(value, values, label) {
|
|
103
|
+
if (typeof value !== 'string' || !values.includes(value)) {
|
|
104
|
+
throw contractError(`${label} is not a supported value`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function assertLegacyRejectedKeys(input, label) {
|
|
109
|
+
for (const key of LEGACY_REJECTED_REQUEST_KEYS) {
|
|
110
|
+
if (Object.hasOwn(input, key)) {
|
|
111
|
+
throw contractError(`${label}.${key} is not supported`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateProtectionObserved(value, label) {
|
|
117
|
+
assertPlainObject(value, label);
|
|
118
|
+
assertNoUnknownKeys(value, ['present', 'active', 'protectionId', 'targetIdentity'], label);
|
|
119
|
+
assertBoolean(value.present, `${label}.present`);
|
|
120
|
+
if (Object.hasOwn(value, 'active')) assertBoolean(value.active, `${label}.active`);
|
|
121
|
+
if (Object.hasOwn(value, 'protectionId') && value.protectionId !== null) {
|
|
122
|
+
assertString(value.protectionId, `${label}.protectionId`);
|
|
123
|
+
}
|
|
124
|
+
if (Object.hasOwn(value, 'targetIdentity') && value.targetIdentity !== null) {
|
|
125
|
+
assertString(value.targetIdentity, `${label}.targetIdentity`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validateProtectedContentReadinessRequest(input) {
|
|
130
|
+
assertPlainObject(input, 'request');
|
|
131
|
+
assertLegacyRejectedKeys(input, 'request');
|
|
132
|
+
assertNoUnknownKeys(input, [
|
|
133
|
+
'contractVersion',
|
|
134
|
+
'platformId',
|
|
135
|
+
'articleKey',
|
|
136
|
+
'inspectionAvailable',
|
|
137
|
+
'blockingRecovery',
|
|
138
|
+
'persistedAuthorityFingerprint',
|
|
139
|
+
'observedAuthorityFingerprint',
|
|
140
|
+
'persistedRepositoryFingerprint',
|
|
141
|
+
'observedRepositoryFingerprint',
|
|
142
|
+
'persistedCandidateFingerprint',
|
|
143
|
+
'observedCandidateFingerprint',
|
|
144
|
+
'protectionObserved',
|
|
145
|
+
'protectionInspectedId',
|
|
146
|
+
'humanAuthorityContextOutcome',
|
|
147
|
+
'addendumPresent',
|
|
148
|
+
'addendumDuplicateLikely',
|
|
149
|
+
'humanDecisionPending',
|
|
150
|
+
'requestId',
|
|
151
|
+
], 'request');
|
|
152
|
+
if (input.contractVersion !== CONTRACT_VERSION) {
|
|
153
|
+
throw contractError('request.contractVersion is unsupported');
|
|
154
|
+
}
|
|
155
|
+
assertString(input.platformId, 'request.platformId');
|
|
156
|
+
assertString(input.articleKey, 'request.articleKey');
|
|
157
|
+
assertBoolean(input.inspectionAvailable, 'request.inspectionAvailable');
|
|
158
|
+
if (Object.hasOwn(input, 'blockingRecovery')) {
|
|
159
|
+
assertBoolean(input.blockingRecovery, 'request.blockingRecovery');
|
|
160
|
+
}
|
|
161
|
+
for (const key of [
|
|
162
|
+
'persistedAuthorityFingerprint',
|
|
163
|
+
'observedAuthorityFingerprint',
|
|
164
|
+
'persistedRepositoryFingerprint',
|
|
165
|
+
'observedRepositoryFingerprint',
|
|
166
|
+
'persistedCandidateFingerprint',
|
|
167
|
+
'observedCandidateFingerprint',
|
|
168
|
+
'protectionInspectedId',
|
|
169
|
+
]) {
|
|
170
|
+
if (Object.hasOwn(input, key)) assertString(input[key], `request.${key}`);
|
|
171
|
+
}
|
|
172
|
+
if (Object.hasOwn(input, 'protectionObserved')) {
|
|
173
|
+
validateProtectionObserved(input.protectionObserved, 'request.protectionObserved');
|
|
174
|
+
}
|
|
175
|
+
if (Object.hasOwn(input, 'humanAuthorityContextOutcome')) {
|
|
176
|
+
assertEnum(
|
|
177
|
+
input.humanAuthorityContextOutcome,
|
|
178
|
+
HUMAN_AUTHORITY_CONTEXT_OUTCOMES,
|
|
179
|
+
'request.humanAuthorityContextOutcome',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (Object.hasOwn(input, 'addendumPresent')) {
|
|
183
|
+
assertBoolean(input.addendumPresent, 'request.addendumPresent');
|
|
184
|
+
}
|
|
185
|
+
if (Object.hasOwn(input, 'addendumDuplicateLikely')) {
|
|
186
|
+
assertBoolean(input.addendumDuplicateLikely, 'request.addendumDuplicateLikely');
|
|
187
|
+
}
|
|
188
|
+
if (Object.hasOwn(input, 'humanDecisionPending')) {
|
|
189
|
+
assertBoolean(input.humanDecisionPending, 'request.humanDecisionPending');
|
|
190
|
+
}
|
|
191
|
+
if (Object.hasOwn(input, 'requestId') && input.requestId !== null) {
|
|
192
|
+
assertString(input.requestId, 'request.requestId');
|
|
193
|
+
}
|
|
194
|
+
return input;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function validateProtectedContentReadinessResponse(input) {
|
|
198
|
+
assertPlainObject(input, 'response');
|
|
199
|
+
assertNoUnknownKeys(input, [
|
|
200
|
+
'contractVersion',
|
|
201
|
+
'success',
|
|
202
|
+
'operation',
|
|
203
|
+
'provider',
|
|
204
|
+
'result',
|
|
205
|
+
'error',
|
|
206
|
+
], 'response');
|
|
207
|
+
if (input.contractVersion !== CONTRACT_VERSION) {
|
|
208
|
+
throw contractError('response.contractVersion is unsupported');
|
|
209
|
+
}
|
|
210
|
+
assertBoolean(input.success, 'response.success');
|
|
211
|
+
if (input.operation !== OPERATION) throw contractError('response.operation is unsupported');
|
|
212
|
+
assertPlainObject(input.provider, 'response.provider');
|
|
213
|
+
assertNoUnknownKeys(input.provider, ['providerId', 'providerVersion'], 'response.provider');
|
|
214
|
+
assertString(input.provider.providerId, 'response.provider.providerId');
|
|
215
|
+
assertString(input.provider.providerVersion, 'response.provider.providerVersion');
|
|
216
|
+
if (input.success) {
|
|
217
|
+
if (input.error !== null) throw contractError('response.error must be null on success');
|
|
218
|
+
assertPlainObject(input.result, 'response.result');
|
|
219
|
+
assertNoUnknownKeys(input.result, ['outcome', 'blockingReasonCodes'], 'response.result');
|
|
220
|
+
assertEnum(input.result.outcome, OUTCOMES, 'response.result.outcome');
|
|
221
|
+
if (!Array.isArray(input.result.blockingReasonCodes)
|
|
222
|
+
|| input.result.blockingReasonCodes.length > MAX_REASONS) {
|
|
223
|
+
throw contractError('response.result.blockingReasonCodes must be a bounded array');
|
|
224
|
+
}
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
input.result.blockingReasonCodes.forEach((reason, index) => {
|
|
227
|
+
assertEnum(reason, BLOCKING_REASON_CODES, `response.result.blockingReasonCodes[${index}]`);
|
|
228
|
+
if (seen.has(reason)) {
|
|
229
|
+
throw contractError(`response.result.blockingReasonCodes[${index}] is duplicated`);
|
|
230
|
+
}
|
|
231
|
+
seen.add(reason);
|
|
232
|
+
});
|
|
233
|
+
} else if (input.error !== null) {
|
|
234
|
+
assertPlainObject(input.error, 'response.error');
|
|
235
|
+
assertNoUnknownKeys(input.error, ['category', 'code', 'message'], 'response.error');
|
|
236
|
+
assertEnum(input.error.category, ERROR_CATEGORIES, 'response.error.category');
|
|
237
|
+
assertEnum(input.error.code, ERROR_CODES, 'response.error.code');
|
|
238
|
+
assertString(input.error.message, 'response.error.message');
|
|
239
|
+
}
|
|
240
|
+
return input;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeProtectedContentReadinessRequest(input) {
|
|
244
|
+
return validateProtectedContentReadinessRequest(input);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeProtectedContentReadinessResponse(input) {
|
|
248
|
+
return validateProtectedContentReadinessResponse(input);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
BLOCKING_REASON_CODES,
|
|
253
|
+
CONTRACT_VERSION,
|
|
254
|
+
ERROR_CATEGORIES,
|
|
255
|
+
ERROR_CODES,
|
|
256
|
+
HUMAN_AUTHORITY_CONTEXT_OUTCOMES,
|
|
257
|
+
LEGACY_REJECTED_REQUEST_KEYS,
|
|
258
|
+
MAX_REASONS,
|
|
259
|
+
MAX_STRING_LENGTH,
|
|
260
|
+
OPERATION,
|
|
261
|
+
OUTCOMES,
|
|
262
|
+
normalizeProtectedContentReadinessRequest,
|
|
263
|
+
normalizeProtectedContentReadinessResponse,
|
|
264
|
+
validateProtectedContentReadinessRequest,
|
|
265
|
+
validateProtectedContentReadinessResponse,
|
|
266
|
+
};
|