@simplexable/kc2-client 0.4.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.4.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",
@@ -24,6 +24,7 @@
24
24
  "./authority/authorityContracts": "./src/authority/authorityContracts.js",
25
25
  "./authority/humanGuidanceContracts": "./src/authority/humanGuidanceContracts.js",
26
26
  "./authority/authorityDispositionContracts": "./src/authority/authorityDispositionContracts.js",
27
+ "./protectedContent/protectedContentReadinessContract": "./src/protectedContent/protectedContentReadinessContract.js",
27
28
  "./persistence/kc2WireOperationContracts": "./src/persistence/kc2WireOperationContracts.js",
28
29
  "./persistence/kc2GenerationSnapshotPersistenceContract": "./src/persistence/kc2GenerationSnapshotPersistenceContract.js",
29
30
  "./persistence/kc2HumanAuthorityReadContract": "./src/persistence/kc2HumanAuthorityReadContract.js",
package/src/index.js CHANGED
@@ -29,6 +29,9 @@ module.exports = {
29
29
  authorityDispositionContracts: require('./authority/authorityDispositionContracts'),
30
30
  humanGuidanceContracts: require('./authority/humanGuidanceContracts'),
31
31
  },
32
+ protectedContent: {
33
+ protectedContentReadinessContract: require('./protectedContent/protectedContentReadinessContract'),
34
+ },
32
35
  persistence: {
33
36
  kc2WireOperationContracts: require('./persistence/kc2WireOperationContracts'),
34
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
+ };