@aikdna/kdna-core 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @aikdna/kdna-core
2
2
 
3
- Pure logic library (zero dependencies) for loading, validating, linting, rendering, and composing KDNA domain cognition packages.
3
+ Core library for loading, validating, linting, rendering, composing, and directly reading KDNA `.kdna` cognition assets. It has zero npm runtime dependencies.
4
4
 
5
5
  ## Installation
6
6
 
@@ -11,7 +11,12 @@ npm install @aikdna/kdna-core
11
11
  ## Usage
12
12
 
13
13
  ```js
14
- const { lintDomain, validateDomainSchema, validateCrossFile, renderDomain } = require('@aikdna/kdna-core');
14
+ const {
15
+ createKdnaAssetReader,
16
+ lintDomain,
17
+ validateDomainSchema,
18
+ validateCrossFile
19
+ } = require('@aikdna/kdna-core');
15
20
 
16
21
  // Validate a domain
17
22
  const dataMap = {
@@ -26,6 +31,65 @@ const crossResult = validateCrossFile(dataMap);
26
31
 
27
32
  ## API
28
33
 
34
+ ### `createKdnaAssetReader()`
35
+
36
+ Direct `.kdna` container reader. The reader opens ZIP-backed `.kdna` assets without persistent extraction and exposes:
37
+
38
+ - `open(pathOrBytes)`
39
+ - `listEntries(asset)`
40
+ - `readEntry(asset, entryName)`
41
+ - `readJson(asset, entryName)`
42
+ - `readManifest(asset)`
43
+ - `readDataMap(asset)`
44
+ - `contentDigest(asset)`
45
+ - `verify(asset, { asset_digest?, content_digest?, requireSignature? })`
46
+ - `loadProfile(asset, "index" | "compact" | "scenario" | "full", options?)`
47
+
48
+ Example:
49
+
50
+ ```js
51
+ const { createKdnaAssetReader } = require('@aikdna/kdna-core');
52
+
53
+ const reader = createKdnaAssetReader();
54
+ const asset = await reader.open('./writing.kdna');
55
+ const manifest = await reader.readManifest(asset);
56
+ const trust = await reader.verify(asset, { requireSignature: true });
57
+ const loaded = await reader.loadProfile(asset, 'compact');
58
+ ```
59
+
60
+ The asset reader treats extraction caches as implementation details. The `.kdna` file remains the identity, install, verification, and loading object.
61
+
62
+ Licensed assets can list encrypted JSON entries in `kdna.json`:
63
+
64
+ ```json
65
+ {
66
+ "access": "licensed",
67
+ "encryption": {
68
+ "profile": "kdna-licensed-entry-v1",
69
+ "encrypted_entries": ["KDNA_Core.json", "KDNA_Patterns.json"]
70
+ }
71
+ }
72
+ ```
73
+
74
+ The reader never writes decrypted entries to disk. Callers provide an in-memory
75
+ `decryptEntry` hook when they have already validated license activation:
76
+
77
+ ```js
78
+ const { createLicensedDecryptEntry } = require('@aikdna/kdna-core');
79
+
80
+ const decryptEntry = createLicensedDecryptEntry({
81
+ licenseKey: activation.license_key,
82
+ machineFingerprint: activation.machine_fingerprint
83
+ });
84
+
85
+ const loaded = await reader.loadProfile(asset, 'compact', { decryptEntry });
86
+ ```
87
+
88
+ The profile uses AES-256-GCM over each protected entry and derives the entry key
89
+ from the license key plus machine fingerprint using `scrypt-sha256`. This is a
90
+ runtime primitive, not a license activation system; callers must validate license
91
+ status before passing a decrypt hook to the reader.
92
+
29
93
  ### `lintDomain(dataMap)`
30
94
  Structural linting — checks required files, field presence, unique IDs, yes/no answerable self-checks, cross-file references, and flags potentially vague axioms.
31
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "KDNA core library — pure logic for loading, validating, linting, and rendering KDNA domain cognition packages. Zero Node.js dependencies.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "types": "src/types.d.ts",
18
18
  "scripts": {
19
- "test": "node -e \"const m = require('./src/index.js'); console.log('kdna-core exports:', Object.keys(m).join(', '));\""
19
+ "test": "node --test test/*.test.js && node -e \"const m = require('./src/index.js'); console.log('kdna-core exports:', Object.keys(m).join(', '));\""
20
20
  },
21
21
  "files": [
22
22
  "src/",
@@ -32,7 +32,7 @@
32
32
  "license": "Apache-2.0",
33
33
  "repository": {
34
34
  "type": "git",
35
- "url": "git+https://github.com/aikdna/KDNA.git",
35
+ "url": "git+https://github.com/aikdna/kdna.git",
36
36
  "directory": "packages/kdna-core"
37
37
  },
38
38
  "homepage": "https://aikdna.com",
@@ -0,0 +1,585 @@
1
+ /**
2
+ * KDNA Asset Reader — direct .kdna container access.
3
+ *
4
+ * This module intentionally uses only Node.js built-ins. It reads ZIP central
5
+ * directory records directly so runtimes can inspect, verify, and load .kdna
6
+ * assets without persistent extraction to a domain directory.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const crypto = require('crypto');
11
+ const zlib = require('zlib');
12
+ const { loadDomainFromFiles, formatContext } = require('./loader');
13
+
14
+ const STANDARD_ENTRIES = [
15
+ 'kdna.json',
16
+ 'KDNA_Core.json',
17
+ 'KDNA_Patterns.json',
18
+ 'KDNA_Scenarios.json',
19
+ 'KDNA_Cases.json',
20
+ 'KDNA_Reasoning.json',
21
+ 'KDNA_Evolution.json',
22
+ ];
23
+
24
+ const JSON_ENTRY_RE = /\.json$/i;
25
+
26
+ function sha256Hex(buf) {
27
+ return crypto.createHash('sha256').update(buf).digest('hex');
28
+ }
29
+
30
+ function stableStringify(value) {
31
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
32
+ if (value && typeof value === 'object') {
33
+ return `{${Object.keys(value)
34
+ .sort()
35
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
36
+ .join(',')}}`;
37
+ }
38
+ return JSON.stringify(value);
39
+ }
40
+
41
+ function parseJson(buf, entryName) {
42
+ try {
43
+ return JSON.parse(Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf));
44
+ } catch (e) {
45
+ throw new Error(`${entryName}: invalid JSON: ${e.message}`);
46
+ }
47
+ }
48
+
49
+ function encryptedEntries(manifest) {
50
+ const entries = manifest?.encryption?.encrypted_entries;
51
+ return Array.isArray(entries) ? entries : [];
52
+ }
53
+
54
+ function isEncryptedEntry(manifest, entryName) {
55
+ return encryptedEntries(manifest).includes(entryName);
56
+ }
57
+
58
+ async function maybeDecryptEntry(asset, manifest, entryName, buf, options = {}) {
59
+ if (!isEncryptedEntry(manifest, entryName)) return buf;
60
+ if (typeof options.decryptEntry !== 'function') {
61
+ throw new Error(`${entryName}: encrypted entry requires decryptEntry hook`);
62
+ }
63
+ const decrypted = await options.decryptEntry({
64
+ asset,
65
+ manifest,
66
+ entryName,
67
+ ciphertext: buf,
68
+ });
69
+ if (typeof decrypted === 'string') return Buffer.from(decrypted);
70
+ if (Buffer.isBuffer(decrypted)) return decrypted;
71
+ if (decrypted instanceof Uint8Array) return Buffer.from(decrypted);
72
+ throw new Error(`${entryName}: decryptEntry hook must return string, Buffer, or Uint8Array`);
73
+ }
74
+
75
+ function normalizeDecryptedEntry(decrypted, entryName) {
76
+ if (typeof decrypted === 'string') return Buffer.from(decrypted);
77
+ if (Buffer.isBuffer(decrypted)) return decrypted;
78
+ if (decrypted instanceof Uint8Array) return Buffer.from(decrypted);
79
+ throw new Error(`${entryName}: decryptEntry hook must return string, Buffer, or Uint8Array`);
80
+ }
81
+
82
+ function maybeDecryptEntrySync(asset, manifest, entryName, buf, options = {}) {
83
+ if (!isEncryptedEntry(manifest, entryName)) return buf;
84
+ if (typeof options.decryptEntry !== 'function') {
85
+ throw new Error(`${entryName}: encrypted entry requires decryptEntry hook`);
86
+ }
87
+ const decrypted = options.decryptEntry({
88
+ asset,
89
+ manifest,
90
+ entryName,
91
+ ciphertext: buf,
92
+ });
93
+ if (decrypted && typeof decrypted.then === 'function') {
94
+ throw new Error(`${entryName}: decryptEntry hook must be synchronous for sync reads`);
95
+ }
96
+ return normalizeDecryptedEntry(decrypted, entryName);
97
+ }
98
+
99
+ function findEndOfCentralDirectory(buf) {
100
+ const min = Math.max(0, buf.length - 65557);
101
+ for (let i = buf.length - 22; i >= min; i--) {
102
+ if (buf.readUInt32LE(i) === 0x06054b50) return i;
103
+ }
104
+ throw new Error('Invalid .kdna asset: ZIP end-of-central-directory not found');
105
+ }
106
+
107
+ function parseZipEntries(buf) {
108
+ const eocd = findEndOfCentralDirectory(buf);
109
+ const totalEntries = buf.readUInt16LE(eocd + 10);
110
+ const centralDirOffset = buf.readUInt32LE(eocd + 16);
111
+ const entries = new Map();
112
+ let offset = centralDirOffset;
113
+
114
+ for (let i = 0; i < totalEntries; i++) {
115
+ if (buf.readUInt32LE(offset) !== 0x02014b50) {
116
+ throw new Error(`Invalid .kdna asset: ZIP central directory is corrupt at ${offset}`);
117
+ }
118
+
119
+ const method = buf.readUInt16LE(offset + 10);
120
+ const compressedSize = buf.readUInt32LE(offset + 20);
121
+ const uncompressedSize = buf.readUInt32LE(offset + 24);
122
+ const nameLen = buf.readUInt16LE(offset + 28);
123
+ const extraLen = buf.readUInt16LE(offset + 30);
124
+ const commentLen = buf.readUInt16LE(offset + 32);
125
+ const localHeaderOffset = buf.readUInt32LE(offset + 42);
126
+ const name = buf.slice(offset + 46, offset + 46 + nameLen).toString('utf8');
127
+
128
+ offset += 46 + nameLen + extraLen + commentLen;
129
+ if (!name || name.endsWith('/')) continue;
130
+
131
+ entries.set(name, {
132
+ name,
133
+ method,
134
+ compressedSize,
135
+ uncompressedSize,
136
+ localHeaderOffset,
137
+ });
138
+ }
139
+
140
+ return entries;
141
+ }
142
+
143
+ function readZipEntry(buf, entry) {
144
+ const offset = entry.localHeaderOffset;
145
+ if (buf.readUInt32LE(offset) !== 0x04034b50) {
146
+ throw new Error(`Invalid .kdna asset: local header missing for ${entry.name}`);
147
+ }
148
+
149
+ const nameLen = buf.readUInt16LE(offset + 26);
150
+ const extraLen = buf.readUInt16LE(offset + 28);
151
+ const dataStart = offset + 30 + nameLen + extraLen;
152
+ const compressed = buf.slice(dataStart, dataStart + entry.compressedSize);
153
+
154
+ if (entry.method === 0) return compressed;
155
+ if (entry.method === 8) return zlib.inflateRawSync(compressed);
156
+ throw new Error(`${entry.name}: unsupported ZIP compression method ${entry.method}`);
157
+ }
158
+
159
+ function normalizeInput(input) {
160
+ if (Buffer.isBuffer(input)) return { buffer: input, path: null };
161
+ if (input instanceof Uint8Array) return { buffer: Buffer.from(input), path: null };
162
+ if (typeof input !== 'string') {
163
+ throw new Error('KdnaAssetReader.open expects a file path, Buffer, or Uint8Array');
164
+ }
165
+ return { buffer: fs.readFileSync(input), path: input };
166
+ }
167
+
168
+ function manifestForDigest(manifest) {
169
+ const copy = { ...(manifest || {}) };
170
+ delete copy.signature;
171
+ delete copy.asset_digest;
172
+ delete copy.container_sha256;
173
+ delete copy.content_digest;
174
+ delete copy._source;
175
+ return copy;
176
+ }
177
+
178
+ function buildContentDigest(asset) {
179
+ const parts = [];
180
+ for (const entryName of [...asset.entries.keys()].sort()) {
181
+ if (entryName === '.DS_Store' || entryName === 'signature.json') continue;
182
+ const entryBuf = asset.readEntry(entryName);
183
+ let digestBuf = entryBuf;
184
+ if (entryName === 'kdna.json') {
185
+ digestBuf = Buffer.from(stableStringify(manifestForDigest(parseJson(entryBuf, entryName))));
186
+ }
187
+ parts.push(`${entryName}:${sha256Hex(digestBuf)}`);
188
+ }
189
+ return `sha256:${sha256Hex(Buffer.from(parts.join('\n')))}`;
190
+ }
191
+
192
+ function manifestForSignature(manifest, { stripDigestFields = true } = {}) {
193
+ const copy = { ...(manifest || {}) };
194
+ delete copy.signature;
195
+ delete copy._source;
196
+ if (stripDigestFields) {
197
+ delete copy.asset_digest;
198
+ delete copy.container_sha256;
199
+ delete copy.content_digest;
200
+ }
201
+ return copy;
202
+ }
203
+
204
+ function buildSigningPayload(asset, options = {}) {
205
+ const parts = [];
206
+ for (const entryName of [...asset.entries.keys()].filter((name) => JSON_ENTRY_RE.test(name)).sort()) {
207
+ if (entryName === 'signature.json') continue;
208
+ const entryBuf = asset.readEntry(entryName);
209
+ let payloadBuf = entryBuf;
210
+ if (entryName === 'kdna.json') {
211
+ payloadBuf = Buffer.from(
212
+ JSON.stringify(manifestForSignature(parseJson(entryBuf, entryName), options)),
213
+ );
214
+ }
215
+ parts.push(`${entryName}:${sha256Hex(payloadBuf)}`);
216
+ }
217
+ return parts.join('\n');
218
+ }
219
+
220
+ function verifySignature(asset, manifest, errors, warnings) {
221
+ if (!manifest.signature) {
222
+ warnings.push('kdna.json.signature missing');
223
+ return null;
224
+ }
225
+ if (!manifest.author?.public_key_pem) {
226
+ errors.push('kdna.json.author.public_key_pem missing');
227
+ return false;
228
+ }
229
+ if (!manifest.author?.pubkey) {
230
+ errors.push('kdna.json.author.pubkey missing');
231
+ return false;
232
+ }
233
+
234
+ const fingerprint = `ed25519:${sha256Hex(Buffer.from(manifest.author.public_key_pem))}`;
235
+ if (fingerprint !== manifest.author.pubkey) {
236
+ errors.push('author.public_key_pem fingerprint does not match author.pubkey');
237
+ return false;
238
+ }
239
+
240
+ try {
241
+ const signature = Buffer.from(String(manifest.signature).replace(/^ed25519:/, ''), 'hex');
242
+ const publicKey = crypto.createPublicKey(manifest.author.public_key_pem);
243
+ let ok = crypto.verify(null, Buffer.from(buildSigningPayload(asset)), publicKey, signature);
244
+ if (!ok) {
245
+ ok = crypto.verify(
246
+ null,
247
+ Buffer.from(buildSigningPayload(asset, { stripDigestFields: false })),
248
+ publicKey,
249
+ signature,
250
+ );
251
+ }
252
+ if (!ok) errors.push('Ed25519 signature invalid');
253
+ return ok;
254
+ } catch (e) {
255
+ errors.push(`signature verification failed: ${e.message}`);
256
+ return false;
257
+ }
258
+ }
259
+
260
+ function openAsset(input) {
261
+ const { buffer, path } = normalizeInput(input);
262
+ const entries = parseZipEntries(buffer);
263
+ return {
264
+ path,
265
+ size: buffer.length,
266
+ asset_digest: `sha256:${sha256Hex(buffer)}`,
267
+ entries,
268
+ readEntry(name) {
269
+ const entry = entries.get(name);
270
+ if (!entry) throw new Error(`Entry not found in .kdna asset: ${name}`);
271
+ return readZipEntry(buffer, entry);
272
+ },
273
+ };
274
+ }
275
+
276
+ function listEntries(asset) {
277
+ return [...asset.entries.keys()].sort();
278
+ }
279
+
280
+ function readEntry(asset, entryName, encoding) {
281
+ const buf = asset.readEntry(entryName);
282
+ return encoding ? buf.toString(encoding) : buf;
283
+ }
284
+
285
+ function readManifest(asset) {
286
+ return parseJson(asset.readEntry('kdna.json'), 'kdna.json');
287
+ }
288
+
289
+ function readDataMapSync(asset, entries = STANDARD_ENTRIES, options = {}) {
290
+ const dataMap = {};
291
+ const manifest = readManifest(asset);
292
+ const encrypted = encryptedEntries(manifest).filter((entryName) => entries.includes(entryName));
293
+ if (encrypted.length && typeof options.decryptEntry !== 'function') {
294
+ throw new Error(`encrypted entries require decryptEntry hook: ${encrypted.join(', ')}`);
295
+ }
296
+ for (const entryName of entries) {
297
+ if (!asset.entries.has(entryName)) continue;
298
+ const buf = maybeDecryptEntrySync(asset, manifest, entryName, asset.readEntry(entryName), options);
299
+ dataMap[entryName] = parseJson(buf, entryName);
300
+ }
301
+ return dataMap;
302
+ }
303
+
304
+ function verifySync(asset, options = {}) {
305
+ const errors = [];
306
+ const warnings = [];
307
+ const entries = listEntries(asset);
308
+
309
+ if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
310
+ if (!asset.entries.has('KDNA_Core.json')) errors.push('required entry missing: KDNA_Core.json');
311
+ if (!asset.entries.has('KDNA_Patterns.json')) {
312
+ errors.push('required entry missing: KDNA_Patterns.json');
313
+ }
314
+
315
+ const content_digest = buildContentDigest(asset);
316
+ const asset_digest = asset.asset_digest;
317
+ if (options.asset_digest && options.asset_digest !== asset_digest) {
318
+ errors.push(`asset digest mismatch: expected ${options.asset_digest}, got ${asset_digest}`);
319
+ }
320
+ if (options.content_digest && options.content_digest !== content_digest) {
321
+ errors.push(`content digest mismatch: expected ${options.content_digest}, got ${content_digest}`);
322
+ }
323
+
324
+ let manifest = null;
325
+ let signature_valid = null;
326
+ if (asset.entries.has('kdna.json')) {
327
+ try {
328
+ manifest = readManifest(asset);
329
+ const encrypted = encryptedEntries(manifest);
330
+ if (encrypted.length) {
331
+ warnings.push(`encrypted entries present: ${encrypted.join(', ')}`);
332
+ if (options.requireDecryption && typeof options.decryptEntry !== 'function') {
333
+ errors.push('decryptEntry hook required for encrypted entries');
334
+ }
335
+ if (typeof options.decryptEntry === 'function') {
336
+ for (const entryName of encrypted) {
337
+ if (!asset.entries.has(entryName)) {
338
+ errors.push(`encrypted entry listed but missing: ${entryName}`);
339
+ continue;
340
+ }
341
+ try {
342
+ const decrypted = maybeDecryptEntrySync(
343
+ asset,
344
+ manifest,
345
+ entryName,
346
+ asset.readEntry(entryName),
347
+ options,
348
+ );
349
+ parseJson(decrypted, entryName);
350
+ } catch (e) {
351
+ errors.push(e.message);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ if (options.requireSignature || manifest.signature) {
357
+ signature_valid = verifySignature(asset, manifest, errors, warnings);
358
+ }
359
+ } catch (e) {
360
+ errors.push(e.message);
361
+ }
362
+ }
363
+
364
+ return {
365
+ ok: errors.length === 0,
366
+ errors,
367
+ warnings,
368
+ entries,
369
+ manifest,
370
+ asset_digest,
371
+ content_digest,
372
+ signature_valid,
373
+ };
374
+ }
375
+
376
+ function loadProfileSync(asset, profile = 'compact', options = {}) {
377
+ const manifest = readManifest(asset);
378
+ if (profile === 'index') {
379
+ return {
380
+ profile,
381
+ manifest,
382
+ asset_digest: asset.asset_digest,
383
+ content_digest: buildContentDigest(asset),
384
+ entries: listEntries(asset),
385
+ name: manifest.name || manifest.asset_id || null,
386
+ version: manifest.version || null,
387
+ judgment_version: manifest.judgment_version || null,
388
+ keywords: manifest.keywords || [],
389
+ };
390
+ }
391
+ const dataMap = readDataMapSync(asset, STANDARD_ENTRIES, options);
392
+ const mode = profile === 'full' ? 'all' : profile === 'scenario' ? 'auto' : 'minimum';
393
+ const domain = loadDomainFromFiles(dataMap, { mode, input: options.input || '' });
394
+ return {
395
+ profile,
396
+ manifest,
397
+ domain,
398
+ context: options.context === false || !domain ? null : formatContext(domain),
399
+ };
400
+ }
401
+
402
+ function createKdnaAssetReader() {
403
+ return {
404
+ openSync: openAsset,
405
+
406
+ async open(input) {
407
+ return openAsset(input);
408
+ },
409
+
410
+ listEntriesSync: listEntries,
411
+
412
+ async listEntries(asset) {
413
+ return listEntries(asset);
414
+ },
415
+
416
+ readEntrySync: readEntry,
417
+
418
+ async readEntry(asset, entryName, encoding) {
419
+ return readEntry(asset, entryName, encoding);
420
+ },
421
+
422
+ readJsonSync(asset, entryName, options = {}) {
423
+ if (!asset.entries.has(entryName)) return null;
424
+ const manifest =
425
+ entryName === 'kdna.json' ? null : parseJson(asset.readEntry('kdna.json'), 'kdna.json');
426
+ const buf = maybeDecryptEntrySync(asset, manifest, entryName, asset.readEntry(entryName), options);
427
+ return parseJson(buf, entryName);
428
+ },
429
+
430
+ async readJson(asset, entryName, options = {}) {
431
+ if (!asset.entries.has(entryName)) return null;
432
+ const manifest =
433
+ entryName === 'kdna.json' ? null : parseJson(asset.readEntry('kdna.json'), 'kdna.json');
434
+ const buf = await maybeDecryptEntry(
435
+ asset,
436
+ manifest,
437
+ entryName,
438
+ asset.readEntry(entryName),
439
+ options,
440
+ );
441
+ return parseJson(buf, entryName);
442
+ },
443
+
444
+ readManifestSync: readManifest,
445
+
446
+ async readManifest(asset) {
447
+ return readManifest(asset);
448
+ },
449
+
450
+ readDataMapSync,
451
+
452
+ async readDataMap(asset, entries = STANDARD_ENTRIES, options = {}) {
453
+ const dataMap = {};
454
+ const manifest = await this.readManifest(asset);
455
+ for (const entryName of entries) {
456
+ if (!asset.entries.has(entryName)) continue;
457
+ const buf = await maybeDecryptEntry(
458
+ asset,
459
+ manifest,
460
+ entryName,
461
+ asset.readEntry(entryName),
462
+ options,
463
+ );
464
+ dataMap[entryName] = parseJson(buf, entryName);
465
+ }
466
+ return dataMap;
467
+ },
468
+
469
+ contentDigestSync: buildContentDigest,
470
+
471
+ async contentDigest(asset) {
472
+ return buildContentDigest(asset);
473
+ },
474
+
475
+ verifySync,
476
+
477
+ async verify(asset, options = {}) {
478
+ const errors = [];
479
+ const warnings = [];
480
+ const entries = [...asset.entries.keys()].sort();
481
+
482
+ if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
483
+ if (!asset.entries.has('KDNA_Core.json')) errors.push('required entry missing: KDNA_Core.json');
484
+ if (!asset.entries.has('KDNA_Patterns.json')) {
485
+ errors.push('required entry missing: KDNA_Patterns.json');
486
+ }
487
+
488
+ const content_digest = buildContentDigest(asset);
489
+ const asset_digest = asset.asset_digest;
490
+ if (options.asset_digest && options.asset_digest !== asset_digest) {
491
+ errors.push(`asset digest mismatch: expected ${options.asset_digest}, got ${asset_digest}`);
492
+ }
493
+ if (options.content_digest && options.content_digest !== content_digest) {
494
+ errors.push(
495
+ `content digest mismatch: expected ${options.content_digest}, got ${content_digest}`,
496
+ );
497
+ }
498
+
499
+ let manifest = null;
500
+ let signature_valid = null;
501
+ if (asset.entries.has('kdna.json')) {
502
+ try {
503
+ manifest = parseJson(asset.readEntry('kdna.json'), 'kdna.json');
504
+ const encrypted = encryptedEntries(manifest);
505
+ if (encrypted.length) {
506
+ warnings.push(`encrypted entries present: ${encrypted.join(', ')}`);
507
+ if (options.requireDecryption && typeof options.decryptEntry !== 'function') {
508
+ errors.push('decryptEntry hook required for encrypted entries');
509
+ }
510
+ if (typeof options.decryptEntry === 'function') {
511
+ for (const entryName of encrypted) {
512
+ if (!asset.entries.has(entryName)) {
513
+ errors.push(`encrypted entry listed but missing: ${entryName}`);
514
+ continue;
515
+ }
516
+ try {
517
+ const decrypted = await maybeDecryptEntry(
518
+ asset,
519
+ manifest,
520
+ entryName,
521
+ asset.readEntry(entryName),
522
+ options,
523
+ );
524
+ parseJson(decrypted, entryName);
525
+ } catch (e) {
526
+ errors.push(e.message);
527
+ }
528
+ }
529
+ }
530
+ }
531
+ if (options.requireSignature || manifest.signature) {
532
+ signature_valid = verifySignature(asset, manifest, errors, warnings);
533
+ }
534
+ } catch (e) {
535
+ errors.push(e.message);
536
+ }
537
+ }
538
+
539
+ return {
540
+ ok: errors.length === 0,
541
+ errors,
542
+ warnings,
543
+ entries,
544
+ manifest,
545
+ asset_digest,
546
+ content_digest,
547
+ signature_valid,
548
+ };
549
+ },
550
+
551
+ loadProfileSync,
552
+
553
+ async loadProfile(asset, profile = 'compact', options = {}) {
554
+ const manifest = await this.readManifest(asset);
555
+ if (profile === 'index') {
556
+ return {
557
+ profile,
558
+ manifest,
559
+ asset_digest: asset.asset_digest,
560
+ content_digest: buildContentDigest(asset),
561
+ entries: await this.listEntries(asset),
562
+ name: manifest.name || manifest.asset_id || null,
563
+ version: manifest.version || null,
564
+ judgment_version: manifest.judgment_version || null,
565
+ keywords: manifest.keywords || [],
566
+ };
567
+ }
568
+
569
+ const dataMap = await this.readDataMap(asset, STANDARD_ENTRIES, options);
570
+ const mode = profile === 'full' ? 'all' : profile === 'scenario' ? 'auto' : 'minimum';
571
+ const domain = loadDomainFromFiles(dataMap, { mode, input: options.input || '' });
572
+ return {
573
+ profile,
574
+ manifest,
575
+ domain,
576
+ context: options.context === false || !domain ? null : formatContext(domain),
577
+ };
578
+ },
579
+ };
580
+ }
581
+
582
+ module.exports = {
583
+ STANDARD_ENTRIES,
584
+ createKdnaAssetReader,
585
+ };
@@ -0,0 +1,106 @@
1
+ const crypto = require('crypto');
2
+
3
+ const LICENSED_ENTRY_PROFILE = 'kdna-licensed-entry-v1';
4
+ const KDF = 'scrypt-sha256';
5
+ const ALG = 'AES-256-GCM';
6
+
7
+ function toBuffer(value, label) {
8
+ if (Buffer.isBuffer(value)) return value;
9
+ if (value instanceof Uint8Array) return Buffer.from(value);
10
+ if (typeof value === 'string') return Buffer.from(value, 'utf8');
11
+ throw new Error(`${label} must be a string, Buffer, or Uint8Array`);
12
+ }
13
+
14
+ function decodeBase64(value, label) {
15
+ if (typeof value !== 'string' || !value) throw new Error(`${label} must be base64`);
16
+ return Buffer.from(value, 'base64');
17
+ }
18
+
19
+ function normalizeEnvelope(value) {
20
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
21
+ return JSON.parse(Buffer.from(value).toString('utf8'));
22
+ }
23
+ if (typeof value === 'string') return JSON.parse(value);
24
+ if (value && typeof value === 'object') return value;
25
+ throw new Error('encrypted entry envelope must be JSON');
26
+ }
27
+
28
+ function deriveLicensedEntryKey(options = {}) {
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 = {}) {
40
+ return Buffer.from(
41
+ [
42
+ LICENSED_ENTRY_PROFILE,
43
+ manifest.name || manifest.asset_id || '',
44
+ manifest.version || '',
45
+ entryName,
46
+ ].join('\n'),
47
+ 'utf8',
48
+ );
49
+ }
50
+
51
+ function encryptLicensedEntry(plaintext, options = {}) {
52
+ const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
53
+ if (!entryName) throw new Error('entryName is required');
54
+ const salt = crypto.randomBytes(16);
55
+ const iv = crypto.randomBytes(12);
56
+ const key = deriveLicensedEntryKey({ licenseKey, machineFingerprint, salt });
57
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
58
+ cipher.setAAD(encryptedEntryAad(entryName, manifest));
59
+ const ciphertext = Buffer.concat([cipher.update(toBuffer(plaintext, 'plaintext')), cipher.final()]);
60
+ return {
61
+ profile: LICENSED_ENTRY_PROFILE,
62
+ alg: ALG,
63
+ kdf: KDF,
64
+ salt: salt.toString('base64'),
65
+ iv: iv.toString('base64'),
66
+ tag: cipher.getAuthTag().toString('base64'),
67
+ ciphertext: ciphertext.toString('base64'),
68
+ };
69
+ }
70
+
71
+ function decryptLicensedEntry(envelopeValue, options = {}) {
72
+ const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
73
+ if (!entryName) throw new Error('entryName is required');
74
+ const envelope = normalizeEnvelope(envelopeValue);
75
+ if (envelope.profile !== LICENSED_ENTRY_PROFILE) {
76
+ throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'}`);
77
+ }
78
+ if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
79
+ if (envelope.kdf !== KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
80
+ const key = deriveLicensedEntryKey({
81
+ licenseKey,
82
+ machineFingerprint,
83
+ salt: envelope.salt,
84
+ });
85
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, decodeBase64(envelope.iv, 'iv'));
86
+ decipher.setAAD(encryptedEntryAad(entryName, manifest));
87
+ decipher.setAuthTag(decodeBase64(envelope.tag, 'tag'));
88
+ return Buffer.concat([
89
+ decipher.update(decodeBase64(envelope.ciphertext, 'ciphertext')),
90
+ decipher.final(),
91
+ ]);
92
+ }
93
+
94
+ function createLicensedDecryptEntry(options = {}) {
95
+ const { licenseKey, machineFingerprint } = options;
96
+ return ({ entryName, ciphertext, manifest }) =>
97
+ decryptLicensedEntry(ciphertext, { entryName, manifest, licenseKey, machineFingerprint });
98
+ }
99
+
100
+ module.exports = {
101
+ LICENSED_ENTRY_PROFILE,
102
+ deriveLicensedEntryKey,
103
+ encryptLicensedEntry,
104
+ decryptLicensedEntry,
105
+ createLicensedDecryptEntry,
106
+ };
package/src/index.js CHANGED
@@ -6,6 +6,8 @@ const lint = require('./lint-pure');
6
6
  const validate = require('./validate-pure');
7
7
  const render = require('./render');
8
8
  const compose = require('./compose');
9
+ const assetReader = require('./asset-reader');
10
+ const cryptoProfile = require('./crypto-profile');
9
11
 
10
12
  module.exports = {
11
13
  ...loader,
@@ -13,4 +15,6 @@ module.exports = {
13
15
  ...validate,
14
16
  ...render,
15
17
  ...compose,
18
+ ...assetReader,
19
+ ...cryptoProfile,
16
20
  };
package/src/index.mjs CHANGED
@@ -17,3 +17,14 @@ export { validateDomainSchema, validateCrossFile } from './validate-pure.js';
17
17
  export { renderPreviewHTML, escHtml, renderCard } from './render.js';
18
18
 
19
19
  export { composeContext, composeContextWithAttribution, classifySignals, classifySignalsAcrossDomains, composeChecks, loadAndCompose, loadCluster, detectDomainConflicts, generateClusterTrace } from './compose.js';
20
+
21
+ import assetReader from './asset-reader.js';
22
+ import cryptoProfile from './crypto-profile.js';
23
+
24
+ export const STANDARD_ENTRIES = assetReader.STANDARD_ENTRIES;
25
+ export const createKdnaAssetReader = assetReader.createKdnaAssetReader;
26
+ export const LICENSED_ENTRY_PROFILE = cryptoProfile.LICENSED_ENTRY_PROFILE;
27
+ export const deriveLicensedEntryKey = cryptoProfile.deriveLicensedEntryKey;
28
+ export const encryptLicensedEntry = cryptoProfile.encryptLicensedEntry;
29
+ export const decryptLicensedEntry = cryptoProfile.decryptLicensedEntry;
30
+ export const createLicensedDecryptEntry = cryptoProfile.createLicensedDecryptEntry;
package/src/types.d.ts CHANGED
@@ -206,12 +206,23 @@ export interface KDNAManifest {
206
206
  kdna_spec: string;
207
207
  name: string;
208
208
  version: string;
209
- status: 'experimental' | 'basic' | 'stable' | 'pro';
209
+ judgment_version?: string;
210
+ status: 'draft' | 'experimental' | 'stable' | 'deprecated' | 'basic' | 'pro';
210
211
  access: 'open' | 'licensed' | 'runtime';
211
- language: string;
212
- author: { name: string; id?: string };
212
+ language?: string;
213
+ default_language?: string;
214
+ languages?: string[];
215
+ author: { name: string; id?: string; pubkey?: string; public_key_pem?: string };
213
216
  license: { type: string; url?: string };
214
217
  description: string;
218
+ keywords?: string[];
219
+ encryption?: {
220
+ profile?: string;
221
+ encrypted_entries?: string[];
222
+ [key: string]: any;
223
+ };
224
+ content_digest?: string;
225
+ signature?: string;
215
226
  }
216
227
 
217
228
  export interface LintResult {
@@ -241,6 +252,167 @@ export function formatContext(domain: LoadedDomain): string;
241
252
 
242
253
  export const FILE_MAP: Record<string, string>;
243
254
 
255
+ // Asset reader — direct .kdna API
256
+ export const STANDARD_ENTRIES: string[];
257
+
258
+ export interface KdnaAsset {
259
+ path: string | null;
260
+ size: number;
261
+ asset_digest: string;
262
+ entries: Map<string, unknown>;
263
+ readEntry(name: string): Uint8Array;
264
+ }
265
+
266
+ export interface KdnaAssetVerifyResult {
267
+ ok: boolean;
268
+ errors: string[];
269
+ warnings: string[];
270
+ entries: string[];
271
+ manifest: KDNAManifest | null;
272
+ asset_digest: string;
273
+ content_digest: string;
274
+ signature_valid: boolean | null;
275
+ }
276
+
277
+ export interface KdnaAssetIndexProfile {
278
+ profile: 'index';
279
+ manifest: KDNAManifest;
280
+ asset_digest: string;
281
+ content_digest: string;
282
+ entries: string[];
283
+ name: string | null;
284
+ version: string | null;
285
+ judgment_version: string | null;
286
+ keywords: string[];
287
+ }
288
+
289
+ export interface KdnaAssetLoadProfile {
290
+ profile: string;
291
+ manifest: KDNAManifest;
292
+ domain: LoadedDomain | null;
293
+ context: string | null;
294
+ }
295
+
296
+ export interface KdnaAssetReader {
297
+ openSync(input: string | Uint8Array): KdnaAsset;
298
+ open(input: string | Uint8Array): Promise<KdnaAsset>;
299
+ listEntriesSync(asset: KdnaAsset): string[];
300
+ listEntries(asset: KdnaAsset): Promise<string[]>;
301
+ readEntrySync(asset: KdnaAsset, entryName: string): Uint8Array;
302
+ readEntrySync(asset: KdnaAsset, entryName: string, encoding: string): string;
303
+ readEntry(asset: KdnaAsset, entryName: string): Promise<Uint8Array>;
304
+ readEntry(asset: KdnaAsset, entryName: string, encoding: string): Promise<string>;
305
+ readJsonSync(asset: KdnaAsset, entryName: string, options?: KdnaDecryptOptions): any;
306
+ readJson(asset: KdnaAsset, entryName: string, options?: KdnaDecryptOptions): Promise<any>;
307
+ readManifestSync(asset: KdnaAsset): KDNAManifest;
308
+ readManifest(asset: KdnaAsset): Promise<KDNAManifest>;
309
+ readDataMapSync(
310
+ asset: KdnaAsset,
311
+ entries?: string[],
312
+ options?: KdnaDecryptOptions,
313
+ ): KDNAFileDataMap;
314
+ readDataMap(
315
+ asset: KdnaAsset,
316
+ entries?: string[],
317
+ options?: KdnaDecryptOptions,
318
+ ): Promise<KDNAFileDataMap>;
319
+ contentDigestSync(asset: KdnaAsset): string;
320
+ contentDigest(asset: KdnaAsset): Promise<string>;
321
+ verifySync(
322
+ asset: KdnaAsset,
323
+ options?: {
324
+ asset_digest?: string;
325
+ content_digest?: string;
326
+ requireSignature?: boolean;
327
+ requireDecryption?: boolean;
328
+ } & KdnaDecryptOptions,
329
+ ): KdnaAssetVerifyResult;
330
+ verify(
331
+ asset: KdnaAsset,
332
+ options?: {
333
+ asset_digest?: string;
334
+ content_digest?: string;
335
+ requireSignature?: boolean;
336
+ requireDecryption?: boolean;
337
+ } & KdnaDecryptOptions,
338
+ ): Promise<KdnaAssetVerifyResult>;
339
+ loadProfileSync(
340
+ asset: KdnaAsset,
341
+ profile: 'index',
342
+ options?: { input?: string; context?: boolean } & KdnaDecryptOptions,
343
+ ): KdnaAssetIndexProfile;
344
+ loadProfileSync(
345
+ asset: KdnaAsset,
346
+ profile?: 'compact' | 'scenario' | 'full' | string,
347
+ options?: { input?: string; context?: boolean } & KdnaDecryptOptions,
348
+ ): KdnaAssetLoadProfile;
349
+ loadProfile(
350
+ asset: KdnaAsset,
351
+ profile: 'index',
352
+ options?: { input?: string; context?: boolean } & KdnaDecryptOptions,
353
+ ): Promise<KdnaAssetIndexProfile>;
354
+ loadProfile(
355
+ asset: KdnaAsset,
356
+ profile?: 'compact' | 'scenario' | 'full' | string,
357
+ options?: { input?: string; context?: boolean } & KdnaDecryptOptions,
358
+ ): Promise<KdnaAssetLoadProfile>;
359
+ }
360
+
361
+ export interface KdnaDecryptOptions {
362
+ decryptEntry?: (args: {
363
+ asset: KdnaAsset;
364
+ manifest: KDNAManifest;
365
+ entryName: string;
366
+ ciphertext: Uint8Array;
367
+ }) => string | Uint8Array | Promise<string | Uint8Array>;
368
+ }
369
+
370
+ export function createKdnaAssetReader(): KdnaAssetReader;
371
+
372
+ export const LICENSED_ENTRY_PROFILE: string;
373
+
374
+ export interface LicensedEntryEnvelope {
375
+ profile: string;
376
+ alg: 'AES-256-GCM';
377
+ kdf: 'scrypt-sha256';
378
+ salt: string;
379
+ iv: string;
380
+ tag: string;
381
+ ciphertext: string;
382
+ }
383
+
384
+ export function deriveLicensedEntryKey(options: {
385
+ licenseKey: string;
386
+ machineFingerprint: string;
387
+ salt: string | Uint8Array;
388
+ keyLength?: number;
389
+ }): Uint8Array;
390
+
391
+ export function encryptLicensedEntry(
392
+ plaintext: string | Uint8Array,
393
+ options: {
394
+ entryName: string;
395
+ manifest?: KDNAManifest;
396
+ licenseKey: string;
397
+ machineFingerprint: string;
398
+ },
399
+ ): LicensedEntryEnvelope;
400
+
401
+ export function decryptLicensedEntry(
402
+ envelope: string | Uint8Array | LicensedEntryEnvelope,
403
+ options: {
404
+ entryName: string;
405
+ manifest?: KDNAManifest;
406
+ licenseKey: string;
407
+ machineFingerprint: string;
408
+ },
409
+ ): Uint8Array;
410
+
411
+ export function createLicensedDecryptEntry(options: {
412
+ licenseKey: string;
413
+ machineFingerprint: string;
414
+ }): NonNullable<KdnaDecryptOptions['decryptEntry']>;
415
+
244
416
  // Lint
245
417
  export function lintDomain(dataMap: KDNAFileDataMap): LintResult;
246
418
 
@@ -45,9 +45,6 @@ function validateDomainSchema(dataMap, schemaMap) {
45
45
  return { valid: true, errors: [], warnings };
46
46
  }
47
47
 
48
- let validCount = 0;
49
- let failCount = 0;
50
-
51
48
  for (const [file, schemaFile] of Object.entries(FILE_TO_SCHEMA)) {
52
49
  if (!dataMap[file]) continue;
53
50
  if (!schemaMap[schemaFile]) {
@@ -69,10 +66,7 @@ function validateDomainSchema(dataMap, schemaMap) {
69
66
  const validate = ajvInstance.compile(schema);
70
67
  const valid = validate(dataMap[file]);
71
68
 
72
- if (valid) {
73
- validCount++;
74
- } else {
75
- failCount++;
69
+ if (!valid) {
76
70
  for (const err of validate.errors || []) {
77
71
  const instancePath = err.instancePath || '/';
78
72
  errors.push(`${file}${instancePath}: ${err.message} (${err.keyword})`);