@aikdna/kdna-cli 0.16.10 → 0.18.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.
@@ -10,6 +10,15 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { lintDomain } = require('@aikdna/kdna-core');
12
12
 
13
+ const KDNA_DOMAIN_FILES = new Set([
14
+ 'KDNA_Core.json',
15
+ 'KDNA_Patterns.json',
16
+ 'KDNA_Scenarios.json',
17
+ 'KDNA_Cases.json',
18
+ 'KDNA_Reasoning.json',
19
+ 'KDNA_Evolution.json',
20
+ ]);
21
+
13
22
  const domainDir = process.argv[2];
14
23
  if (!domainDir || domainDir === '--help' || domainDir === '-h') {
15
24
  console.log(`kdna-lint — Structural and content validation for KDNA domains.
@@ -28,19 +37,44 @@ if (!fs.existsSync(domainDir) || !fs.statSync(domainDir).isDirectory()) {
28
37
  process.exit(2);
29
38
  }
30
39
 
31
- // Read all KDNA JSON files in the domain directory
32
- const files = fs.readdirSync(domainDir).filter((f) => f.endsWith('.json') && f !== 'kdna.json');
40
+ // Read only canonical domain content files. Governance metadata such as
41
+ // KDNA_CARD.json is part of the package, but not one of the 6 KDNA JSON files.
42
+ const files = fs.readdirSync(domainDir).filter((f) => KDNA_DOMAIN_FILES.has(f));
33
43
  const dataMap = {};
34
44
  for (const f of files) {
35
45
  try {
36
46
  dataMap[f] = JSON.parse(fs.readFileSync(path.join(domainDir, f), 'utf8'));
37
- } catch (e) {
47
+ } catch {
38
48
  // lintDomain will report missing required files; skip unparseable here
39
49
  }
40
50
  }
41
51
 
42
52
  const result = lintDomain(dataMap);
43
53
 
54
+ // Also validate kdna.json manifest if present and validateManifest is available
55
+ let manifestPath;
56
+ try {
57
+ manifestPath = path.join(domainDir, 'kdna.json');
58
+ if (fs.existsSync(manifestPath)) {
59
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
60
+ let validateManifestFn;
61
+ try {
62
+ validateManifestFn = require('@aikdna/kdna-core').validateManifest;
63
+ } catch {
64
+ // validateManifest not yet available in installed kdna-core — skip manifest check
65
+ }
66
+ if (validateManifestFn) {
67
+ const mResult = validateManifestFn(manifest);
68
+ for (const e of mResult.errors) result.errors.push(`kdna.json: ${e}`);
69
+ for (const w of mResult.warnings) result.warnings.push(`kdna.json: ${w}`);
70
+ }
71
+ }
72
+ } catch (e) {
73
+ if (e.code !== 'ENOENT') {
74
+ result.errors.push(`kdna.json: failed to parse — ${e.message}`);
75
+ }
76
+ }
77
+
44
78
  if (result.warnings.length) {
45
79
  console.log('Warnings:');
46
80
  result.warnings.forEach((w) => console.log(` - ${w}`));
@@ -42,7 +42,8 @@ const FILE_MAP = {
42
42
  'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
43
43
  };
44
44
 
45
- // Read all KDNA JSON files
45
+ // Read only canonical domain content files. Governance metadata such as
46
+ // KDNA_CARD.json is valid package metadata, but not part of the 6-file domain set.
46
47
  const dataMap = {};
47
48
  for (const [file] of Object.entries(FILE_MAP)) {
48
49
  const filePath = path.join(domainDir, file);
@@ -57,7 +58,7 @@ for (const [file] of Object.entries(FILE_MAP)) {
57
58
 
58
59
  // Read schemas
59
60
  const schemaMap = {};
60
- for (const [file, schemaFile] of Object.entries(FILE_MAP)) {
61
+ for (const schemaFile of Object.values(FILE_MAP)) {
61
62
  const schemaPath = path.join(SCHEMA_DIR, schemaFile);
62
63
  if (fs.existsSync(schemaPath)) {
63
64
  try {
@@ -1,199 +0,0 @@
1
- /**
2
- * KDNA Encryption — Encrypted container format (.kdnae) for licensed domains.
3
- *
4
- * .kdnae extends .kdna with AES-256-GCM encryption on KDNA JSON files.
5
- * The kdna.json manifest and license.json stay in plaintext for discovery.
6
- *
7
- * Encryption key is derived via PBKDF2 from:
8
- * license_key + machine_fingerprint
9
- *
10
- * This module provides pure-crypto functions (no filesystem I/O).
11
- * File operations are in domain.js (pack/unpack) and install.js.
12
- */
13
-
14
- const crypto = require('crypto');
15
-
16
- const ALGORITHM = 'aes-256-gcm';
17
- const IV_LENGTH = 16; // 96-bit IV for GCM
18
- const TAG_LENGTH = 16; // 128-bit auth tag
19
- const SALT_LENGTH = 32;
20
- const PBKDF2_ITERATIONS = 600000;
21
- const KEY_LENGTH = 32; // AES-256
22
-
23
- // Files in a .kdna container that get encrypted (JSON content only)
24
- const ENCRYPTED_FILES = [
25
- 'KDNA_Core.json',
26
- 'KDNA_Patterns.json',
27
- 'KDNA_Scenarios.json',
28
- 'KDNA_Cases.json',
29
- 'KDNA_Reasoning.json',
30
- 'KDNA_Evolution.json',
31
- ];
32
-
33
- // Files that always stay in plaintext
34
- const PLAINTEXT_FILES = ['kdna.json', 'license.json', 'README.md', 'LICENSE', 'signature.json'];
35
-
36
- function isEncryptable(filename) {
37
- return ENCRYPTED_FILES.includes(filename);
38
- }
39
-
40
- // ─── Machine Fingerprint ────────────────────────────────────────────────
41
-
42
- function machineFingerprint() {
43
- const os = require('os');
44
- const parts = [os.hostname(), os.userInfo().uid.toString(), os.platform(), os.arch()];
45
- // Try to get hardware UUID on macOS
46
- try {
47
- const { execSync } = require('child_process');
48
- if (os.platform() === 'darwin') {
49
- const uuid = execSync('ioreg -d2 -c IOPlatformExpertDevice | grep IOPlatformUUID', {
50
- encoding: 'utf8',
51
- timeout: 3000,
52
- })
53
- .match(/"[A-F0-9-]{36}"/)?.[0]
54
- ?.replace(/"/g, '');
55
- if (uuid) parts.push(uuid);
56
- }
57
- if (os.platform() === 'linux') {
58
- try {
59
- const mid = require('fs').readFileSync('/etc/machine-id', 'utf8').trim();
60
- if (mid) parts.push(mid);
61
- } catch {
62
- /* ignore */
63
- }
64
- }
65
- } catch {
66
- /* non-critical */
67
- }
68
- return crypto.createHash('sha256').update(parts.join('|')).digest('hex');
69
- }
70
-
71
- // ─── Key Derivation ─────────────────────────────────────────────────────
72
-
73
- function deriveKey(licenseKey, fingerprint) {
74
- const salt = crypto
75
- .createHash('sha256')
76
- .update(fingerprint || machineFingerprint())
77
- .digest();
78
- return crypto.pbkdf2Sync(licenseKey, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha512');
79
- }
80
-
81
- // ─── Encrypt / Decrypt ──────────────────────────────────────────────────
82
-
83
- function encrypt(plaintext, key) {
84
- const iv = crypto.randomBytes(IV_LENGTH);
85
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
86
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
87
- const tag = cipher.getAuthTag();
88
- // Format: iv (16) + tag (16) + ciphertext
89
- return Buffer.concat([iv, tag, encrypted]);
90
- }
91
-
92
- function decrypt(encryptedData, key) {
93
- if (encryptedData.length < IV_LENGTH + TAG_LENGTH) {
94
- throw new Error('Invalid encrypted data: too short');
95
- }
96
- const iv = encryptedData.subarray(0, IV_LENGTH);
97
- const tag = encryptedData.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
98
- const ciphertext = encryptedData.subarray(IV_LENGTH + TAG_LENGTH);
99
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
100
- decipher.setAuthTag(tag);
101
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
102
- }
103
-
104
- // ─── Encrypted Container Detection ──────────────────────────────────────
105
-
106
- function isEncryptedContainer(filePath) {
107
- return filePath.endsWith('.kdnae');
108
- }
109
-
110
- // ─── License File ──────────────────────────────────────────────────────
111
-
112
- function createLicense(domain, options = {}) {
113
- const {
114
- issuedTo = 'licensee@example.com',
115
- expiresAt = null, // ISO date or null for perpetual
116
- maxAgents = 1,
117
- requireMachineBinding = true,
118
- requireOnlineCheck = false,
119
- offlineGraceDays = 7,
120
- } = options;
121
-
122
- const license = {
123
- version: '1.0',
124
- license_id: `lic_${crypto.randomBytes(8).toString('hex')}`,
125
- domain,
126
- issued_to: issuedTo,
127
- issued_at: new Date().toISOString(),
128
- expires_at: expiresAt,
129
- max_agents: maxAgents,
130
- require_machine_binding: requireMachineBinding,
131
- require_online_check: requireOnlineCheck,
132
- offline_grace_days: offlineGraceDays,
133
- allowed_agents: options.allowedAgents || ['claude_code', 'codex', 'opencode'],
134
- };
135
-
136
- return license;
137
- }
138
-
139
- function verifyLicense(license, scopePubkey, fingerprint) {
140
- const issues = [];
141
-
142
- // Check expiration
143
- if (license.expires_at) {
144
- if (new Date(license.expires_at) < new Date()) {
145
- issues.push('License has expired');
146
- }
147
- }
148
-
149
- // Check machine binding
150
- if (license.require_machine_binding) {
151
- if (license.machine_fingerprint && license.machine_fingerprint !== fingerprint) {
152
- issues.push('Machine fingerprint mismatch');
153
- }
154
- }
155
-
156
- return {
157
- valid: issues.length === 0,
158
- issues,
159
- domain: license.domain,
160
- license_id: license.license_id,
161
- issued_to: license.issued_to,
162
- };
163
- }
164
-
165
- function signLicense(license, privateKeyPem) {
166
- const { signature: _, ...payload } = license;
167
- const data = JSON.stringify(payload, Object.keys(payload).sort());
168
- // Use crypto.sign() for Ed25519 (supported via PEM keys)
169
- const sig = crypto.sign(null, Buffer.from(data), privateKeyPem);
170
- license.signature = `ed25519:${sig.toString('hex')}`;
171
- return license;
172
- }
173
-
174
- function verifyLicenseSignature(license, publicKeyPem) {
175
- const signature = license.signature?.replace('ed25519:', '') || '';
176
- if (!signature) return false;
177
- const { signature: _, ...payload } = license;
178
- const data = JSON.stringify(payload, Object.keys(payload).sort());
179
- try {
180
- return crypto.verify(null, Buffer.from(data), publicKeyPem, Buffer.from(signature, 'hex'));
181
- } catch {
182
- return false;
183
- }
184
- }
185
-
186
- module.exports = {
187
- encrypt,
188
- decrypt,
189
- deriveKey,
190
- machineFingerprint,
191
- isEncryptable,
192
- isEncryptedContainer,
193
- createLicense,
194
- verifyLicense,
195
- signLicense,
196
- verifyLicenseSignature,
197
- ENCRYPTED_FILES,
198
- PLAINTEXT_FILES,
199
- };