@aikdna/kdna-core 0.5.0 → 0.7.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/README.md +53 -1
- package/package.json +1 -1
- package/schema/KDNA_Scenarios.schema.json +2 -2
- package/schema/kdna-file.schema.json +16 -6
- package/schema/kdna-manifest-v1rc.json +346 -0
- package/src/asset-reader.js +51 -17
- package/src/crypto-profile.js +278 -25
- package/src/index.js +2 -0
- package/src/lint-pure.js +41 -10
- package/src/public-api.js +323 -0
- package/src/types.d.ts +76 -5
package/src/crypto-profile.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
|
|
3
|
+
// ── Profile constants ──────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RFC-0008 compliant profile.
|
|
7
|
+
* - HKDF-SHA256 key derivation
|
|
8
|
+
* - AES-256-KW (RFC 3394) content encryption key wrapping
|
|
9
|
+
* - AES-256-GCM content encryption
|
|
10
|
+
* - Random CEK per asset; license key only unwraps CEK, never touches content
|
|
11
|
+
*/
|
|
3
12
|
const LICENSED_ENTRY_PROFILE = 'kdna-licensed-entry-v1';
|
|
4
|
-
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pre-RFC legacy profile (now experimental).
|
|
16
|
+
* Uses scrypt-sha256 with concatenated secret (`licenseKey|machineFingerprint`).
|
|
17
|
+
* Retained for backward compatibility only.
|
|
18
|
+
*/
|
|
19
|
+
const LICENSED_EXPERIMENTAL_PROFILE = 'kdna-licensed-entry-experimental';
|
|
20
|
+
|
|
21
|
+
const RFC_KDF = 'HKDF-SHA256';
|
|
22
|
+
const RFC_KEY_WRAPPING = 'AES-256-KW';
|
|
23
|
+
const LEGACY_KDF = 'scrypt-sha256';
|
|
5
24
|
const ALG = 'AES-256-GCM';
|
|
6
25
|
|
|
26
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
27
|
+
|
|
7
28
|
function toBuffer(value, label) {
|
|
8
29
|
if (Buffer.isBuffer(value)) return value;
|
|
9
30
|
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
@@ -25,21 +46,10 @@ function normalizeEnvelope(value) {
|
|
|
25
46
|
throw new Error('encrypted entry envelope must be JSON');
|
|
26
47
|
}
|
|
27
48
|
|
|
28
|
-
function
|
|
29
|
-
const { licenseKey, machineFingerprint, salt, keyLength = 32 } = options;
|
|
30
|
-
if (!licenseKey) throw new Error('licenseKey is required');
|
|
31
|
-
if (!machineFingerprint) throw new Error('machineFingerprint is required');
|
|
32
|
-
const saltBuffer = Buffer.isBuffer(salt) || salt instanceof Uint8Array
|
|
33
|
-
? Buffer.from(salt)
|
|
34
|
-
: decodeBase64(salt, 'salt');
|
|
35
|
-
const secret = `${licenseKey}|${machineFingerprint}`;
|
|
36
|
-
return crypto.scryptSync(secret, saltBuffer, keyLength);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function encryptedEntryAad(entryName, manifest = {}) {
|
|
49
|
+
function encryptedEntryAad(entryName, manifest = {}, profile = LICENSED_ENTRY_PROFILE) {
|
|
40
50
|
return Buffer.from(
|
|
41
51
|
[
|
|
42
|
-
|
|
52
|
+
profile,
|
|
43
53
|
manifest.name || manifest.asset_id || '',
|
|
44
54
|
manifest.version || '',
|
|
45
55
|
entryName,
|
|
@@ -48,19 +58,220 @@ function encryptedEntryAad(entryName, manifest = {}) {
|
|
|
48
58
|
);
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
// ── HKDF-SHA256 (RFC 5869) ────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* HKDF-SHA256 extract-then-expand.
|
|
65
|
+
* @param {Buffer|string} ikm — input keying material (license key)
|
|
66
|
+
* @param {Buffer} [salt] — optional salt (defaults to 32 zero bytes)
|
|
67
|
+
* @param {Buffer|string} [info] — context info
|
|
68
|
+
* @param {number} [length] — output length (default 32)
|
|
69
|
+
*/
|
|
70
|
+
function hkdfSha256(ikm, salt = null, info = '', length = 32) {
|
|
71
|
+
const ikmBuf = toBuffer(ikm, 'ikm');
|
|
72
|
+
// Extract
|
|
73
|
+
const saltBuf = salt || Buffer.alloc(32, 0);
|
|
74
|
+
const prk = crypto.createHmac('sha256', saltBuf).update(ikmBuf).digest();
|
|
75
|
+
// Expand (single-block expansion for length ≤ 32)
|
|
76
|
+
const t = crypto.createHmac('sha256', prk)
|
|
77
|
+
.update(Buffer.concat([
|
|
78
|
+
toBuffer(info, 'info'),
|
|
79
|
+
// Counter byte 0x01
|
|
80
|
+
Buffer.from([1]),
|
|
81
|
+
]))
|
|
82
|
+
.digest();
|
|
83
|
+
return t.subarray(0, length);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── AES-256-KW (RFC 3394) ─────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const AES_BLOCK = 16;
|
|
89
|
+
const KW_IV = Buffer.from('a6a6a6a6a6a6a6a6', 'hex'); // RFC 3394 default IV
|
|
90
|
+
|
|
91
|
+
function aesWrap(key, plaintext) {
|
|
92
|
+
// key = 32 bytes, plaintext = 32 bytes (single CEK)
|
|
93
|
+
if (key.length !== 32) throw new Error('AES-256-KW requires 32-byte key');
|
|
94
|
+
if (plaintext.length !== 32) throw new Error('AES-256-KW requires 32-byte plaintext');
|
|
95
|
+
|
|
96
|
+
const n = plaintext.length / 8; // = 4 for 256-bit CEK
|
|
97
|
+
const r = new Array(n + 1);
|
|
98
|
+
r[0] = KW_IV;
|
|
99
|
+
for (let i = 0; i < n; i++) r[i + 1] = plaintext.subarray(i * 8, (i + 1) * 8);
|
|
100
|
+
|
|
101
|
+
for (let j = 0; j <= 5; j++) {
|
|
102
|
+
for (let i = 1; i <= n; i++) {
|
|
103
|
+
const input = Buffer.concat([r[0], r[i]]); // 16 bytes
|
|
104
|
+
const cipher = crypto.createCipheriv('aes-256-ecb', key, null);
|
|
105
|
+
cipher.setAutoPadding(false);
|
|
106
|
+
const b = Buffer.concat([cipher.update(input), cipher.final()]);
|
|
107
|
+
r[0] = b.subarray(0, 8);
|
|
108
|
+
// XOR t = (n * j) + i as big-endian 64-bit
|
|
109
|
+
const t = BigInt(n) * BigInt(j) + BigInt(i);
|
|
110
|
+
const tBuf = Buffer.alloc(8);
|
|
111
|
+
tBuf.writeBigUInt64BE(t);
|
|
112
|
+
for (let k = 0; k < 8; k++) r[0][k] ^= tBuf[k];
|
|
113
|
+
r[i] = b.subarray(8, 16);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = Buffer.alloc((n + 1) * 8);
|
|
118
|
+
for (let i = 0; i <= n; i++) r[i].copy(result, i * 8);
|
|
119
|
+
return result; // 40 bytes
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function aesUnwrap(key, ciphertext) {
|
|
123
|
+
if (key.length !== 32) throw new Error('AES-256-KW requires 32-byte key');
|
|
124
|
+
if (ciphertext.length !== 40) throw new Error('AES-256-KW ciphertext must be 40 bytes');
|
|
125
|
+
|
|
126
|
+
const n = (ciphertext.length / 8) - 1; // = 4
|
|
127
|
+
const r = new Array(n + 1);
|
|
128
|
+
for (let i = 0; i <= n; i++) r[i] = ciphertext.subarray(i * 8, (i + 1) * 8);
|
|
129
|
+
|
|
130
|
+
for (let j = 5; j >= 0; j--) {
|
|
131
|
+
for (let i = n; i >= 1; i--) {
|
|
132
|
+
const t = BigInt(n) * BigInt(j) + BigInt(i);
|
|
133
|
+
const tBuf = Buffer.alloc(8);
|
|
134
|
+
tBuf.writeBigUInt64BE(t);
|
|
135
|
+
for (let k = 0; k < 8; k++) r[0][k] ^= tBuf[k];
|
|
136
|
+
|
|
137
|
+
const input = Buffer.concat([r[0], r[i]]);
|
|
138
|
+
const decipher = crypto.createDecipheriv('aes-256-ecb', key, null);
|
|
139
|
+
decipher.setAutoPadding(false);
|
|
140
|
+
const b = Buffer.concat([decipher.update(input), decipher.final()]);
|
|
141
|
+
r[0] = b.subarray(0, 8);
|
|
142
|
+
r[i] = b.subarray(8, 16);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!r[0].equals(KW_IV)) throw new Error('AES-256-KW unwrap: integrity check failed');
|
|
147
|
+
const result = Buffer.alloc(n * 8);
|
|
148
|
+
for (let i = 1; i <= n; i++) r[i].copy(result, (i - 1) * 8);
|
|
149
|
+
return result; // 32 bytes
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── RFC-0008 compliant encryption ─────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Derive a key-wrapping key (KWK) from a license key via HKDF-SHA256.
|
|
156
|
+
*/
|
|
157
|
+
function deriveWrappingKey(licenseKey, info = 'kdna-licensed-entry-v1-kwk') {
|
|
158
|
+
return hkdfSha256(licenseKey, null, info, 32);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate a random content encryption key (CEK).
|
|
163
|
+
*/
|
|
164
|
+
function generateCEK() {
|
|
165
|
+
return crypto.randomBytes(32);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wrap a CEK with a key-wrapping key.
|
|
170
|
+
*/
|
|
171
|
+
function wrapCEK(cek, wrappingKey) {
|
|
172
|
+
return aesWrap(wrappingKey, cek);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Unwrap a CEK from its wrapped form.
|
|
177
|
+
*/
|
|
178
|
+
function unwrapCEK(wrappedCek, wrappingKey) {
|
|
179
|
+
return aesUnwrap(wrappingKey, wrappedCek);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Encrypt a licensed entry using the RFC-0008 compliant model.
|
|
184
|
+
*
|
|
185
|
+
* 1. Generate random CEK
|
|
186
|
+
* 2. Derive KWK from license key + machine fingerprint via HKDF
|
|
187
|
+
* 3. Encrypt content with CEK (AES-256-GCM)
|
|
188
|
+
* 4. Wrap CEK with KWK (AES-256-KW)
|
|
189
|
+
* 5. Store wrapped_key in envelope
|
|
190
|
+
*/
|
|
191
|
+
function encryptLicensedEntryV1(plaintext, options = {}) {
|
|
192
|
+
const { entryName, manifest = {}, licenseKey } = options;
|
|
193
|
+
if (!entryName) throw new Error('entryName is required');
|
|
194
|
+
if (!licenseKey) throw new Error('licenseKey is required for RFC-0008 encryption');
|
|
195
|
+
|
|
196
|
+
const cek = generateCEK();
|
|
197
|
+
const wrappingKey = deriveWrappingKey(licenseKey);
|
|
198
|
+
const wrappedKey = wrapCEK(cek, wrappingKey);
|
|
199
|
+
|
|
200
|
+
const iv = crypto.randomBytes(12);
|
|
201
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', cek, iv);
|
|
202
|
+
cipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
203
|
+
const ciphertext = Buffer.concat([cipher.update(toBuffer(plaintext, 'plaintext')), cipher.final()]);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
profile: LICENSED_ENTRY_PROFILE,
|
|
207
|
+
alg: ALG,
|
|
208
|
+
kdf: RFC_KDF,
|
|
209
|
+
key_wrapping: RFC_KEY_WRAPPING,
|
|
210
|
+
wrapped_key: wrappedKey.toString('base64'),
|
|
211
|
+
iv: iv.toString('base64'),
|
|
212
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
213
|
+
ciphertext: ciphertext.toString('base64'),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Decrypt a licensed entry encoded with the RFC-0008 compliant profile.
|
|
219
|
+
*
|
|
220
|
+
* 1. Derive KWK from license key via HKDF
|
|
221
|
+
* 2. Unwrap CEK from wrapped_key (AES-256-KW)
|
|
222
|
+
* 3. Decrypt content with CEK (AES-256-GCM)
|
|
223
|
+
*/
|
|
224
|
+
function decryptLicensedEntryV1(envelopeValue, options = {}) {
|
|
225
|
+
const { entryName, manifest = {}, licenseKey } = options;
|
|
226
|
+
if (!entryName) throw new Error('entryName is required');
|
|
227
|
+
if (!licenseKey) throw new Error('licenseKey is required for RFC-0008 decryption');
|
|
228
|
+
|
|
229
|
+
const envelope = normalizeEnvelope(envelopeValue);
|
|
230
|
+
if (envelope.profile !== LICENSED_ENTRY_PROFILE) {
|
|
231
|
+
throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'} (expected ${LICENSED_ENTRY_PROFILE})`);
|
|
232
|
+
}
|
|
233
|
+
if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
|
|
234
|
+
if (envelope.kdf !== RFC_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
|
|
235
|
+
if (envelope.key_wrapping !== RFC_KEY_WRAPPING) throw new Error(`unsupported encrypted entry key_wrapping: ${envelope.key_wrapping}`);
|
|
236
|
+
|
|
237
|
+
const wrappingKey = deriveWrappingKey(licenseKey);
|
|
238
|
+
const cek = unwrapCEK(decodeBase64(envelope.wrapped_key, 'wrapped_key'), wrappingKey);
|
|
239
|
+
|
|
240
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', cek, decodeBase64(envelope.iv, 'iv'));
|
|
241
|
+
decipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
242
|
+
decipher.setAuthTag(decodeBase64(envelope.tag, 'tag'));
|
|
243
|
+
return Buffer.concat([
|
|
244
|
+
decipher.update(decodeBase64(envelope.ciphertext, 'ciphertext')),
|
|
245
|
+
decipher.final(),
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Legacy / experimental profile (pre-RFC, backward compat) ───────
|
|
250
|
+
|
|
251
|
+
function deriveLicensedEntryKeyLegacy(options = {}) {
|
|
252
|
+
const { licenseKey, machineFingerprint, salt, keyLength = 32 } = options;
|
|
253
|
+
if (!licenseKey) throw new Error('licenseKey is required');
|
|
254
|
+
if (!machineFingerprint) throw new Error('machineFingerprint is required');
|
|
255
|
+
const saltBuffer = Buffer.isBuffer(salt) || salt instanceof Uint8Array
|
|
256
|
+
? Buffer.from(salt)
|
|
257
|
+
: decodeBase64(salt, 'salt');
|
|
258
|
+
const secret = `${licenseKey}|${machineFingerprint}`;
|
|
259
|
+
return crypto.scryptSync(secret, saltBuffer, keyLength);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function encryptLicensedEntryLegacy(plaintext, options = {}) {
|
|
52
263
|
const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
|
|
53
264
|
if (!entryName) throw new Error('entryName is required');
|
|
54
265
|
const salt = crypto.randomBytes(16);
|
|
55
266
|
const iv = crypto.randomBytes(12);
|
|
56
|
-
const key =
|
|
267
|
+
const key = deriveLicensedEntryKeyLegacy({ licenseKey, machineFingerprint, salt });
|
|
57
268
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
58
|
-
cipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
269
|
+
cipher.setAAD(encryptedEntryAad(entryName, manifest, LICENSED_EXPERIMENTAL_PROFILE));
|
|
59
270
|
const ciphertext = Buffer.concat([cipher.update(toBuffer(plaintext, 'plaintext')), cipher.final()]);
|
|
60
271
|
return {
|
|
61
|
-
profile:
|
|
272
|
+
profile: LICENSED_EXPERIMENTAL_PROFILE,
|
|
62
273
|
alg: ALG,
|
|
63
|
-
kdf:
|
|
274
|
+
kdf: LEGACY_KDF,
|
|
64
275
|
salt: salt.toString('base64'),
|
|
65
276
|
iv: iv.toString('base64'),
|
|
66
277
|
tag: cipher.getAuthTag().toString('base64'),
|
|
@@ -68,22 +279,22 @@ function encryptLicensedEntry(plaintext, options = {}) {
|
|
|
68
279
|
};
|
|
69
280
|
}
|
|
70
281
|
|
|
71
|
-
function
|
|
282
|
+
function decryptLicensedEntryLegacy(envelopeValue, options = {}) {
|
|
72
283
|
const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
|
|
73
284
|
if (!entryName) throw new Error('entryName is required');
|
|
74
285
|
const envelope = normalizeEnvelope(envelopeValue);
|
|
75
|
-
if (envelope.profile !==
|
|
286
|
+
if (envelope.profile !== LICENSED_EXPERIMENTAL_PROFILE) {
|
|
76
287
|
throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'}`);
|
|
77
288
|
}
|
|
78
289
|
if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
|
|
79
|
-
if (envelope.kdf !==
|
|
80
|
-
const key =
|
|
290
|
+
if (envelope.kdf !== LEGACY_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
|
|
291
|
+
const key = deriveLicensedEntryKeyLegacy({
|
|
81
292
|
licenseKey,
|
|
82
293
|
machineFingerprint,
|
|
83
294
|
salt: envelope.salt,
|
|
84
295
|
});
|
|
85
296
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, decodeBase64(envelope.iv, 'iv'));
|
|
86
|
-
decipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
297
|
+
decipher.setAAD(encryptedEntryAad(entryName, manifest, LICENSED_EXPERIMENTAL_PROFILE));
|
|
87
298
|
decipher.setAuthTag(decodeBase64(envelope.tag, 'tag'));
|
|
88
299
|
return Buffer.concat([
|
|
89
300
|
decipher.update(decodeBase64(envelope.ciphertext, 'ciphertext')),
|
|
@@ -91,6 +302,23 @@ function decryptLicensedEntry(envelopeValue, options = {}) {
|
|
|
91
302
|
]);
|
|
92
303
|
}
|
|
93
304
|
|
|
305
|
+
// ── Unified entry points (auto-detect profile) ────────────────────
|
|
306
|
+
|
|
307
|
+
function encryptLicensedEntry(plaintext, options = {}) {
|
|
308
|
+
return encryptLicensedEntryV1(plaintext, options);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function decryptLicensedEntry(envelopeValue, options = {}) {
|
|
312
|
+
const envelope = normalizeEnvelope(envelopeValue);
|
|
313
|
+
if (envelope.profile === LICENSED_ENTRY_PROFILE) {
|
|
314
|
+
return decryptLicensedEntryV1(envelopeValue, options);
|
|
315
|
+
}
|
|
316
|
+
if (envelope.profile === LICENSED_EXPERIMENTAL_PROFILE) {
|
|
317
|
+
return decryptLicensedEntryLegacy(envelopeValue, options);
|
|
318
|
+
}
|
|
319
|
+
throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
94
322
|
function createLicensedDecryptEntry(options = {}) {
|
|
95
323
|
const { licenseKey, machineFingerprint } = options;
|
|
96
324
|
return ({ entryName, ciphertext, manifest }) =>
|
|
@@ -98,9 +326,34 @@ function createLicensedDecryptEntry(options = {}) {
|
|
|
98
326
|
}
|
|
99
327
|
|
|
100
328
|
module.exports = {
|
|
329
|
+
// Profiles
|
|
101
330
|
LICENSED_ENTRY_PROFILE,
|
|
102
|
-
|
|
331
|
+
LICENSED_EXPERIMENTAL_PROFILE,
|
|
332
|
+
ALG,
|
|
333
|
+
RFC_KDF,
|
|
334
|
+
RFC_KEY_WRAPPING,
|
|
335
|
+
LEGACY_KDF,
|
|
336
|
+
|
|
337
|
+
// RFC-0008 compliant
|
|
338
|
+
deriveWrappingKey,
|
|
339
|
+
generateCEK,
|
|
340
|
+
wrapCEK,
|
|
341
|
+
unwrapCEK,
|
|
342
|
+
encryptLicensedEntryV1,
|
|
343
|
+
decryptLicensedEntryV1,
|
|
344
|
+
|
|
345
|
+
// Legacy
|
|
346
|
+
deriveLicensedEntryKey: deriveLicensedEntryKeyLegacy,
|
|
347
|
+
encryptLicensedEntryLegacy,
|
|
348
|
+
decryptLicensedEntryLegacy,
|
|
349
|
+
|
|
350
|
+
// Unified
|
|
103
351
|
encryptLicensedEntry,
|
|
104
352
|
decryptLicensedEntry,
|
|
105
353
|
createLicensedDecryptEntry,
|
|
354
|
+
|
|
355
|
+
// Low-level
|
|
356
|
+
hkdfSha256,
|
|
357
|
+
aesWrap,
|
|
358
|
+
aesUnwrap,
|
|
106
359
|
};
|
package/src/index.js
CHANGED
|
@@ -8,8 +8,10 @@ const render = require('./render');
|
|
|
8
8
|
const compose = require('./compose');
|
|
9
9
|
const assetReader = require('./asset-reader');
|
|
10
10
|
const cryptoProfile = require('./crypto-profile');
|
|
11
|
+
const publicApi = require('./public-api');
|
|
11
12
|
|
|
12
13
|
module.exports = {
|
|
14
|
+
...publicApi,
|
|
13
15
|
...loader,
|
|
14
16
|
...lint,
|
|
15
17
|
...validate,
|
package/src/lint-pure.js
CHANGED
|
@@ -261,11 +261,20 @@ const VALID_BADGE = new Set(['untested', 'tested', 'validated', 'expert_reviewed
|
|
|
261
261
|
const VALID_ACCESS = new Set(['open', 'licensed', 'runtime']);
|
|
262
262
|
const VALID_RISK = new Set(['R0', 'R1', 'R2', 'R3']);
|
|
263
263
|
const VALID_I18N = new Set(['L0', 'L1', 'L2', 'L3']);
|
|
264
|
+
const VALID_PRIVACY = new Set(['public', 'private', 'sensitive', 'regulated']);
|
|
265
|
+
const VALID_ASSET_TYPE = new Set([
|
|
266
|
+
'domain_judgment',
|
|
267
|
+
'personal_judgment',
|
|
268
|
+
'organization_standard',
|
|
269
|
+
'team_policy',
|
|
270
|
+
'creator_style',
|
|
271
|
+
'risk_guard',
|
|
272
|
+
]);
|
|
264
273
|
|
|
265
274
|
const MANIFEST_REQUIRED = [
|
|
266
|
-
'
|
|
267
|
-
'description', 'author', 'license', 'status',
|
|
268
|
-
'quality_badge', 'access', '
|
|
275
|
+
'format', 'format_version', 'spec_version', 'name', 'version',
|
|
276
|
+
'judgment_version', 'description', 'author', 'license', 'status',
|
|
277
|
+
'quality_badge', 'access', 'languages', 'default_language',
|
|
269
278
|
];
|
|
270
279
|
|
|
271
280
|
/**
|
|
@@ -283,13 +292,15 @@ function validateManifest(manifest) {
|
|
|
283
292
|
return { errors, warnings };
|
|
284
293
|
}
|
|
285
294
|
|
|
286
|
-
// 1. Check
|
|
287
|
-
if ('
|
|
295
|
+
// 1. Check disallowed pre-v1.0 manifest aliases
|
|
296
|
+
if ('kdna_spec' in manifest) {
|
|
288
297
|
errors.push(
|
|
289
|
-
'kdna.json:
|
|
290
|
-
'(spec_version is reserved for .kdna container manifests only.)',
|
|
298
|
+
'kdna.json: kdna_spec is not allowed. Use spec_version.',
|
|
291
299
|
);
|
|
292
300
|
}
|
|
301
|
+
if ('language' in manifest) {
|
|
302
|
+
errors.push('kdna.json: language is not allowed. Use default_language and languages.');
|
|
303
|
+
}
|
|
293
304
|
|
|
294
305
|
// 2. Check required fields
|
|
295
306
|
for (const field of MANIFEST_REQUIRED) {
|
|
@@ -304,6 +315,14 @@ function validateManifest(manifest) {
|
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
// 4. Validate enum fields
|
|
318
|
+
if (manifest.format && manifest.format !== 'kdna') {
|
|
319
|
+
errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
|
|
320
|
+
}
|
|
321
|
+
if (manifest.format_version && manifest.format_version !== '1.0') {
|
|
322
|
+
errors.push(
|
|
323
|
+
`kdna.json.format_version: invalid value "${manifest.format_version}". Expected "1.0".`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
307
326
|
if (manifest.status && !VALID_STATUS.has(manifest.status)) {
|
|
308
327
|
errors.push(
|
|
309
328
|
`kdna.json.status: invalid value "${manifest.status}". ` +
|
|
@@ -334,6 +353,18 @@ function validateManifest(manifest) {
|
|
|
334
353
|
`Valid: ${[...VALID_I18N].join(', ')}`,
|
|
335
354
|
);
|
|
336
355
|
}
|
|
356
|
+
if (manifest.privacy_level && !VALID_PRIVACY.has(manifest.privacy_level)) {
|
|
357
|
+
warnings.push(
|
|
358
|
+
`kdna.json.privacy_level: non-standard value "${manifest.privacy_level}". ` +
|
|
359
|
+
`Valid: ${[...VALID_PRIVACY].join(', ')}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (manifest.asset_type && !VALID_ASSET_TYPE.has(manifest.asset_type)) {
|
|
363
|
+
warnings.push(
|
|
364
|
+
`kdna.json.asset_type: non-standard value "${manifest.asset_type}". ` +
|
|
365
|
+
`Valid: ${[...VALID_ASSET_TYPE].join(', ')}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
337
368
|
|
|
338
369
|
// 5. Deprecated status must have replaced_by
|
|
339
370
|
if (manifest.status === 'deprecated' && !manifest.replaced_by) {
|
|
@@ -362,10 +393,10 @@ function validateManifest(manifest) {
|
|
|
362
393
|
errors.push('kdna.json.license: missing "type"');
|
|
363
394
|
}
|
|
364
395
|
|
|
365
|
-
// 9. Validate
|
|
366
|
-
if (manifest.
|
|
396
|
+
// 9. Validate spec_version value
|
|
397
|
+
if (manifest.spec_version && manifest.spec_version !== '1.0-rc') {
|
|
367
398
|
warnings.push(
|
|
368
|
-
`kdna.json.
|
|
399
|
+
`kdna.json.spec_version: non-standard value "${manifest.spec_version}". Expected "1.0-rc".`,
|
|
369
400
|
);
|
|
370
401
|
}
|
|
371
402
|
|