@aikdna/kdna-cli 0.13.0 → 0.15.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 +1 -1
- package/src/cli.js +48 -4
- package/src/cmds/_common.js +2 -1
- package/src/cmds/domain.js +237 -0
- package/src/cmds/encrypt.js +190 -0
- package/src/cmds/license.js +205 -0
- package/src/install.js +98 -4
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { error, EXIT, setQuiet, setExitCodeOnly } = require('./cmds/_common');
|
|
10
|
-
const { cmdValidate, cmdPack, cmdUnpack, cmdInspect } = require('./cmds/domain');
|
|
10
|
+
const { cmdValidate, cmdPack, cmdPackEncrypt, cmdUnpack, cmdUnpackEncrypt, cmdInspect } = require('./cmds/domain');
|
|
11
11
|
const { cmdList, cmdRegistry } = require('./cmds/registry');
|
|
12
12
|
const {
|
|
13
13
|
cmdCompare,
|
|
@@ -24,6 +24,7 @@ const { cmdIdentity } = require('./cmds/identity');
|
|
|
24
24
|
const { cmdSetup } = require('./cmds/setup');
|
|
25
25
|
const { cmdDoctor } = require('./cmds/doctor');
|
|
26
26
|
const { cmdTrace, cmdHistory } = require('./cmds/trace');
|
|
27
|
+
const { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow, cmdLicenseInstall } = require('./cmds/license');
|
|
27
28
|
const { cmdPreview, cmdProject, cmdEval, cmdExport, cmdDemo } = require('./cmds/legacy');
|
|
28
29
|
const { cmdStudioScaffold, cmdCardsValidate, cmdLockVerify, cmdStudioCompile, cmdStudioReadiness } = require('./cmds/studio');
|
|
29
30
|
const { cmdTestRun, cmdTestImport } = require('./cmds/test');
|
|
@@ -54,7 +55,9 @@ Domain Authoring:
|
|
|
54
55
|
validate <path> Validate domain structure
|
|
55
56
|
validate --schema <path> Schema-only validation
|
|
56
57
|
pack <path> Pack into .kdna container
|
|
58
|
+
pack <path> --encrypt --license <file> Pack encrypted .kdnae container
|
|
57
59
|
unpack <file> Unpack .kdna container
|
|
60
|
+
unpack <file> --license <file> Unpack encrypted .kdnae container
|
|
58
61
|
inspect <path> Inspect domain or .kdna file
|
|
59
62
|
publish <path> Pack + sign + publish
|
|
60
63
|
publish --check <path> Quality gate check only
|
|
@@ -133,6 +136,13 @@ Trace & Diagnostics:
|
|
|
133
136
|
trace [--json] [--since 7d] [--export <file>] Agent judgment trace
|
|
134
137
|
history [--stats] [--domain <name>] [--agent <name>] Recent usage
|
|
135
138
|
|
|
139
|
+
License & Authorization:
|
|
140
|
+
license generate <domain> --to <email> Generate signed license
|
|
141
|
+
license install <license.json> Register license for auto-decrypt
|
|
142
|
+
license verify <license.json> Verify license signature
|
|
143
|
+
license bind <license.json> Bind license to this machine
|
|
144
|
+
license show <license.json> Display license details
|
|
145
|
+
|
|
136
146
|
Flags:
|
|
137
147
|
--json Structured JSON output (machine-readable)
|
|
138
148
|
--quiet Suppress non-error output
|
|
@@ -167,13 +177,21 @@ switch (cmd) {
|
|
|
167
177
|
}
|
|
168
178
|
}
|
|
169
179
|
if (!target) error('Usage: kdna pack <path>');
|
|
170
|
-
|
|
180
|
+
if (args.includes('--encrypt')) {
|
|
181
|
+
cmdPackEncrypt(target, args);
|
|
182
|
+
} else {
|
|
183
|
+
cmdPack(target, output);
|
|
184
|
+
}
|
|
171
185
|
break;
|
|
172
186
|
}
|
|
173
187
|
case 'unpack': {
|
|
174
188
|
const target = args[1];
|
|
175
|
-
if (!target) error('Usage: kdna unpack <file.kdna>');
|
|
176
|
-
|
|
189
|
+
if (!target) error('Usage: kdna unpack <file.kdna|file.kdnae>');
|
|
190
|
+
if (target.endsWith('.kdnae')) {
|
|
191
|
+
cmdUnpackEncrypt(target, args);
|
|
192
|
+
} else {
|
|
193
|
+
cmdUnpack(target, args.includes('--force'));
|
|
194
|
+
}
|
|
177
195
|
break;
|
|
178
196
|
}
|
|
179
197
|
case 'preview': {
|
|
@@ -408,6 +426,32 @@ switch (cmd) {
|
|
|
408
426
|
cmdHistory(args);
|
|
409
427
|
break;
|
|
410
428
|
}
|
|
429
|
+
case 'license': {
|
|
430
|
+
const sub = args[1];
|
|
431
|
+
const rest = args.slice(2);
|
|
432
|
+
if (sub === 'generate') {
|
|
433
|
+
cmdLicenseGenerate(rest);
|
|
434
|
+
} else if (sub === 'verify') {
|
|
435
|
+
cmdLicenseVerify(rest);
|
|
436
|
+
} else if (sub === 'bind') {
|
|
437
|
+
cmdLicenseBind(rest);
|
|
438
|
+
} else if (sub === 'show') {
|
|
439
|
+
cmdLicenseShow(rest);
|
|
440
|
+
} else if (sub === 'install') {
|
|
441
|
+
cmdLicenseInstall(rest);
|
|
442
|
+
} else {
|
|
443
|
+
error(
|
|
444
|
+
'Usage:\n' +
|
|
445
|
+
' kdna license generate <domain> --to <email> [--expires <date>]\n' +
|
|
446
|
+
' kdna license install <license.json>\n' +
|
|
447
|
+
' kdna license verify <license.json>\n' +
|
|
448
|
+
' kdna license bind <license.json>\n' +
|
|
449
|
+
' kdna license show <license.json>',
|
|
450
|
+
EXIT.INPUT_ERROR,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
411
455
|
case 'identity': {
|
|
412
456
|
cmdIdentity(args);
|
|
413
457
|
break;
|
package/src/cmds/_common.js
CHANGED
|
@@ -57,8 +57,9 @@ Usage:
|
|
|
57
57
|
kdna validate <path> Validate a domain directory
|
|
58
58
|
kdna validate --schema <path> ...with JSON Schema
|
|
59
59
|
kdna pack <path> Pack a domain folder into a .kdna container
|
|
60
|
+
kdna pack <path> --encrypt --license <file> Pack encrypted .kdnae container
|
|
60
61
|
kdna pack --output <dir> <path> Output .kdna to specific directory
|
|
61
|
-
kdna unpack <path> Unpack a .kdna container to a folder
|
|
62
|
+
kdna unpack <path> Unpack a .kdna or .kdnae container to a folder
|
|
62
63
|
kdna inspect <path> Inspect a domain directory or .kdna file
|
|
63
64
|
kdna publish <path> Pack + sign + output registry patch
|
|
64
65
|
kdna publish <path> --release-tag <tag> --repo <o/r> ...also upload to GitHub
|
package/src/cmds/domain.js
CHANGED
|
@@ -2,6 +2,14 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const { error, readJson, writeJson, EXIT } = require('./_common');
|
|
5
|
+
const {
|
|
6
|
+
encrypt,
|
|
7
|
+
decrypt,
|
|
8
|
+
deriveKey,
|
|
9
|
+
machineFingerprint,
|
|
10
|
+
isEncryptable,
|
|
11
|
+
ENCRYPTED_FILES,
|
|
12
|
+
} = require('./encrypt');
|
|
5
13
|
|
|
6
14
|
// ─── Validate ────────────────────────────────────────────────────────
|
|
7
15
|
|
|
@@ -707,9 +715,238 @@ function cmdInspect(dir, jsonMode = false) {
|
|
|
707
715
|
console.log('═'.repeat(50));
|
|
708
716
|
}
|
|
709
717
|
|
|
718
|
+
// ─── Encrypted Container (.kdnae) ─────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
function cmdPackEncrypt(dir, args = []) {
|
|
721
|
+
const abs = path.resolve(dir);
|
|
722
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
723
|
+
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
727
|
+
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
728
|
+
if (!core) error('KDNA_Core.json not found or invalid');
|
|
729
|
+
if (!pat) error('KDNA_Patterns.json not found or invalid');
|
|
730
|
+
|
|
731
|
+
const domainName = core.meta?.domain || path.basename(abs);
|
|
732
|
+
|
|
733
|
+
let manifest = readJson(path.join(abs, 'kdna.json'));
|
|
734
|
+
if (!manifest) {
|
|
735
|
+
const jsonCount = fs.readdirSync(abs).filter(f => f.endsWith('.json') && f !== 'kdna.json').length;
|
|
736
|
+
manifest = {
|
|
737
|
+
kdna_spec: '1.0-rc',
|
|
738
|
+
name: domainName,
|
|
739
|
+
version: core.meta?.version || '0.1.0',
|
|
740
|
+
status: 'experimental',
|
|
741
|
+
access: 'licensed',
|
|
742
|
+
language: 'en',
|
|
743
|
+
author: { name: '', id: '' },
|
|
744
|
+
license: { type: 'KCL-1.0' },
|
|
745
|
+
description: core.meta?.purpose || `${domainName} domain cognition`,
|
|
746
|
+
file_count: jsonCount,
|
|
747
|
+
created: new Date().toISOString().slice(0, 10),
|
|
748
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
749
|
+
};
|
|
750
|
+
writeJson(path.join(abs, 'kdna.json'), manifest);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Get encryption key
|
|
754
|
+
const licenseIdx = args.indexOf('--license');
|
|
755
|
+
const keyIdx = args.indexOf('--key');
|
|
756
|
+
let encKey;
|
|
757
|
+
|
|
758
|
+
if (licenseIdx >= 0) {
|
|
759
|
+
const licensePath = args[licenseIdx + 1];
|
|
760
|
+
if (!licensePath || !fs.existsSync(licensePath)) error('License file not found', EXIT.INPUT_ERROR);
|
|
761
|
+
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
762
|
+
const licenseKey = license.license_id;
|
|
763
|
+
const fp = machineFingerprint();
|
|
764
|
+
encKey = deriveKey(licenseKey, fp);
|
|
765
|
+
} else if (keyIdx >= 0) {
|
|
766
|
+
const rawKey = args[keyIdx + 1];
|
|
767
|
+
if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
|
|
768
|
+
encKey = deriveKey(rawKey, machineFingerprint());
|
|
769
|
+
} else {
|
|
770
|
+
error('Use --license <license.json> or --key <secret> to provide encryption key', EXIT.INPUT_ERROR);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const outputDir = args.includes('--output') ? args[args.indexOf('--output') + 1] : null;
|
|
774
|
+
const outName = `${domainName}.kdnae`;
|
|
775
|
+
const outPath = outputDir ? path.join(outputDir, outName) : path.join(process.cwd(), outName);
|
|
776
|
+
if (outputDir && !fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
777
|
+
|
|
778
|
+
// Build encrypted ZIP
|
|
779
|
+
const zlib = require('zlib');
|
|
780
|
+
const files = fs.readdirSync(abs).filter(f => {
|
|
781
|
+
if (f.startsWith('.')) return false;
|
|
782
|
+
const ext = path.extname(f);
|
|
783
|
+
return ext === '.json' || f === 'README.md' || f === 'LICENSE' || f === 'kdna.json';
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const centralDir = [];
|
|
787
|
+
const fileData = [];
|
|
788
|
+
let offset = 0;
|
|
789
|
+
|
|
790
|
+
for (const f of files.sort()) {
|
|
791
|
+
let raw = fs.readFileSync(path.join(abs, f));
|
|
792
|
+
let storedName = f;
|
|
793
|
+
|
|
794
|
+
if (isEncryptable(f)) {
|
|
795
|
+
try {
|
|
796
|
+
const encrypted = encrypt(raw.toString('utf8'), encKey);
|
|
797
|
+
raw = encrypted;
|
|
798
|
+
storedName = f; // Keep original name, content is encrypted
|
|
799
|
+
} catch (err) {
|
|
800
|
+
error(`Failed to encrypt ${f}: ${err.message}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const crc = crc32(raw);
|
|
805
|
+
const compressed = zlib.deflateRawSync(raw);
|
|
806
|
+
const useStore = compressed.length >= raw.length;
|
|
807
|
+
const stored = useStore ? raw : compressed;
|
|
808
|
+
|
|
809
|
+
const nameBytes = Buffer.from(storedName, 'utf8');
|
|
810
|
+
const localHeader = Buffer.alloc(30);
|
|
811
|
+
localHeader.writeUInt32LE(0x04034b50, 0);
|
|
812
|
+
localHeader.writeUInt16LE(20, 4);
|
|
813
|
+
localHeader.writeUInt16LE(0x0800, 6);
|
|
814
|
+
localHeader.writeUInt16LE(useStore ? 0 : 8, 8);
|
|
815
|
+
localHeader.writeUInt32LE(crc, 14);
|
|
816
|
+
localHeader.writeUInt32LE(useStore ? raw.length : compressed.length, 18);
|
|
817
|
+
localHeader.writeUInt32LE(raw.length, 22);
|
|
818
|
+
localHeader.writeUInt16LE(nameBytes.length, 26);
|
|
819
|
+
|
|
820
|
+
fileData.push(Buffer.concat([localHeader, nameBytes, stored]));
|
|
821
|
+
offset += localHeader.length + nameBytes.length + stored.length;
|
|
822
|
+
|
|
823
|
+
const cdEntry = Buffer.alloc(46);
|
|
824
|
+
cdEntry.writeUInt32LE(0x02014b50, 0);
|
|
825
|
+
cdEntry.writeUInt16LE(20, 4);
|
|
826
|
+
cdEntry.writeUInt16LE(20, 6);
|
|
827
|
+
cdEntry.writeUInt16LE(0x0800, 8);
|
|
828
|
+
cdEntry.writeUInt16LE(useStore ? 0 : 8, 10);
|
|
829
|
+
cdEntry.writeUInt32LE(crc, 16);
|
|
830
|
+
cdEntry.writeUInt32LE(useStore ? raw.length : compressed.length, 20);
|
|
831
|
+
cdEntry.writeUInt32LE(raw.length, 24);
|
|
832
|
+
cdEntry.writeUInt16LE(nameBytes.length, 28);
|
|
833
|
+
cdEntry.writeUInt32LE(offset - stored.length - nameBytes.length - localHeader.length, 42);
|
|
834
|
+
centralDir.push(Buffer.concat([cdEntry, nameBytes]));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const cdOffset = offset;
|
|
838
|
+
const cdSize = centralDir.reduce((s, e) => s + e.length, 0);
|
|
839
|
+
const eocd = Buffer.alloc(22);
|
|
840
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
841
|
+
eocd.writeUInt16LE(0, 4);
|
|
842
|
+
eocd.writeUInt16LE(0, 6);
|
|
843
|
+
eocd.writeUInt16LE(files.length, 8);
|
|
844
|
+
eocd.writeUInt16LE(files.length, 10);
|
|
845
|
+
eocd.writeUInt32LE(cdSize, 12);
|
|
846
|
+
eocd.writeUInt32LE(cdOffset, 16);
|
|
847
|
+
eocd.writeUInt16LE(0, 20);
|
|
848
|
+
|
|
849
|
+
const all = Buffer.concat([...fileData, ...centralDir, eocd]);
|
|
850
|
+
fs.writeFileSync(outPath, all);
|
|
851
|
+
|
|
852
|
+
console.log(`✓ Encrypted pack: ${outPath}`);
|
|
853
|
+
console.log(` Domain: ${domainName} v${manifest.version}`);
|
|
854
|
+
console.log(` Files: ${files.length} (${files.filter(isEncryptable).length} encrypted, ${files.filter(f => !isEncryptable(f)).length} plaintext)`);
|
|
855
|
+
console.log(` Container: AES-256-GCM .kdnae`);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function cmdUnpackEncrypt(filePath, args = []) {
|
|
859
|
+
const abs = path.resolve(filePath);
|
|
860
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
861
|
+
error(`Not a file: ${abs}`, EXIT.INPUT_ERROR);
|
|
862
|
+
}
|
|
863
|
+
if (!abs.endsWith('.kdnae')) {
|
|
864
|
+
error(`Not a .kdnae file: ${abs}`, EXIT.INPUT_ERROR);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const licenseIdx = args.indexOf('--license');
|
|
868
|
+
const keyIdx = args.indexOf('--key');
|
|
869
|
+
let encKey;
|
|
870
|
+
|
|
871
|
+
if (licenseIdx >= 0) {
|
|
872
|
+
const licensePath = args[licenseIdx + 1];
|
|
873
|
+
if (!licensePath || !fs.existsSync(licensePath)) error('License file not found', EXIT.INPUT_ERROR);
|
|
874
|
+
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
875
|
+
const licenseKey = license.license_id;
|
|
876
|
+
const fp = machineFingerprint();
|
|
877
|
+
encKey = deriveKey(licenseKey, fp);
|
|
878
|
+
} else if (keyIdx >= 0) {
|
|
879
|
+
const rawKey = args[keyIdx + 1];
|
|
880
|
+
if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
|
|
881
|
+
encKey = deriveKey(rawKey, machineFingerprint());
|
|
882
|
+
} else {
|
|
883
|
+
error('Use --license <license.json> or --key <secret> to decrypt', EXIT.INPUT_ERROR);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const domainName = path.basename(abs, '.kdnae');
|
|
887
|
+
const outDir = path.join(path.dirname(abs), domainName);
|
|
888
|
+
const force = args.includes('--force');
|
|
889
|
+
|
|
890
|
+
if (fs.existsSync(outDir)) {
|
|
891
|
+
if (!force) error(`Directory already exists: ${outDir}\nUse --force to overwrite.`, EXIT.INPUT_ERROR);
|
|
892
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
893
|
+
}
|
|
894
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
895
|
+
|
|
896
|
+
// Extract ZIP first, then decrypt KDNA JSON files
|
|
897
|
+
const os = require('os');
|
|
898
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdnae-unpack-'));
|
|
899
|
+
|
|
900
|
+
try {
|
|
901
|
+
const tmpPy = path.join(os.tmpdir(), `kdnae-unpack-${Date.now()}.py`);
|
|
902
|
+
try {
|
|
903
|
+
const script = `import zipfile, os
|
|
904
|
+
zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
|
|
905
|
+
zf.extractall(${JSON.stringify(tmpDir)})
|
|
906
|
+
zf.close()
|
|
907
|
+
`;
|
|
908
|
+
fs.writeFileSync(tmpPy, script);
|
|
909
|
+
execSync(`python3 ${tmpPy}`, { stdio: 'pipe' });
|
|
910
|
+
} catch {
|
|
911
|
+
try { execSync(`unzip -q -o "${abs}" -d "${tmpDir}"`, { stdio: 'pipe' }); }
|
|
912
|
+
catch { error('Cannot unpack .kdnae container'); }
|
|
913
|
+
} finally {
|
|
914
|
+
try { fs.unlinkSync(tmpPy); } catch { /* cleanup */ }
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Copy plaintext files, decrypt KDNA files
|
|
918
|
+
const extracted = fs.readdirSync(tmpDir);
|
|
919
|
+
for (const f of extracted) {
|
|
920
|
+
const src = path.join(tmpDir, f);
|
|
921
|
+
const dest = path.join(outDir, f);
|
|
922
|
+
|
|
923
|
+
if (ENCRYPTED_FILES.includes(f)) {
|
|
924
|
+
try {
|
|
925
|
+
const encrypted = fs.readFileSync(src);
|
|
926
|
+
const decrypted = decrypt(encrypted, encKey);
|
|
927
|
+
fs.writeFileSync(dest, decrypted);
|
|
928
|
+
} catch (err) {
|
|
929
|
+
error(`Failed to decrypt ${f}: ${err.message}. Wrong license key?`);
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
fs.copyFileSync(src, dest);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
} finally {
|
|
936
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* cleanup */ }
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
console.log(`✓ Decrypted: ${outDir}`);
|
|
940
|
+
const files = fs.readdirSync(outDir);
|
|
941
|
+
console.log(` Files: ${files.length}`);
|
|
942
|
+
files.forEach(f => console.log(` ${f}`));
|
|
943
|
+
}
|
|
944
|
+
|
|
710
945
|
module.exports = {
|
|
711
946
|
cmdValidate,
|
|
712
947
|
cmdPack,
|
|
948
|
+
cmdPackEncrypt,
|
|
713
949
|
cmdUnpack,
|
|
950
|
+
cmdUnpackEncrypt,
|
|
714
951
|
cmdInspect,
|
|
715
952
|
};
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
}).match(/"[A-F0-9-]{36}"/)?.[0]?.replace(/"/g, '');
|
|
53
|
+
if (uuid) parts.push(uuid);
|
|
54
|
+
}
|
|
55
|
+
if (os.platform() === 'linux') {
|
|
56
|
+
try {
|
|
57
|
+
const mid = require('fs').readFileSync('/etc/machine-id', 'utf8').trim();
|
|
58
|
+
if (mid) parts.push(mid);
|
|
59
|
+
} catch { /* ignore */ }
|
|
60
|
+
}
|
|
61
|
+
} catch { /* non-critical */ }
|
|
62
|
+
return crypto.createHash('sha256').update(parts.join('|')).digest('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Key Derivation ─────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function deriveKey(licenseKey, fingerprint) {
|
|
68
|
+
const salt = crypto.createHash('sha256').update(fingerprint || machineFingerprint()).digest();
|
|
69
|
+
return crypto.pbkdf2Sync(licenseKey, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha512');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Encrypt / Decrypt ──────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function encrypt(plaintext, key) {
|
|
75
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
76
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
77
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
78
|
+
const tag = cipher.getAuthTag();
|
|
79
|
+
// Format: iv (16) + tag (16) + ciphertext
|
|
80
|
+
return Buffer.concat([iv, tag, encrypted]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function decrypt(encryptedData, key) {
|
|
84
|
+
if (encryptedData.length < IV_LENGTH + TAG_LENGTH) {
|
|
85
|
+
throw new Error('Invalid encrypted data: too short');
|
|
86
|
+
}
|
|
87
|
+
const iv = encryptedData.subarray(0, IV_LENGTH);
|
|
88
|
+
const tag = encryptedData.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
89
|
+
const ciphertext = encryptedData.subarray(IV_LENGTH + TAG_LENGTH);
|
|
90
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
91
|
+
decipher.setAuthTag(tag);
|
|
92
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Encrypted Container Detection ──────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function isEncryptedContainer(filePath) {
|
|
98
|
+
return filePath.endsWith('.kdnae');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── License File ──────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function createLicense(domain, options = {}) {
|
|
104
|
+
const {
|
|
105
|
+
issuedTo = 'licensee@example.com',
|
|
106
|
+
expiresAt = null, // ISO date or null for perpetual
|
|
107
|
+
maxAgents = 1,
|
|
108
|
+
requireMachineBinding = true,
|
|
109
|
+
requireOnlineCheck = false,
|
|
110
|
+
offlineGraceDays = 7,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
const license = {
|
|
114
|
+
version: '1.0',
|
|
115
|
+
license_id: `lic_${crypto.randomBytes(8).toString('hex')}`,
|
|
116
|
+
domain,
|
|
117
|
+
issued_to: issuedTo,
|
|
118
|
+
issued_at: new Date().toISOString(),
|
|
119
|
+
expires_at: expiresAt,
|
|
120
|
+
max_agents: maxAgents,
|
|
121
|
+
require_machine_binding: requireMachineBinding,
|
|
122
|
+
require_online_check: requireOnlineCheck,
|
|
123
|
+
offline_grace_days: offlineGraceDays,
|
|
124
|
+
allowed_agents: options.allowedAgents || ['claude_code', 'codex', 'opencode'],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return license;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function verifyLicense(license, scopePubkey, fingerprint) {
|
|
131
|
+
const issues = [];
|
|
132
|
+
|
|
133
|
+
// Check expiration
|
|
134
|
+
if (license.expires_at) {
|
|
135
|
+
if (new Date(license.expires_at) < new Date()) {
|
|
136
|
+
issues.push('License has expired');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check machine binding
|
|
141
|
+
if (license.require_machine_binding) {
|
|
142
|
+
if (license.machine_fingerprint && license.machine_fingerprint !== fingerprint) {
|
|
143
|
+
issues.push('Machine fingerprint mismatch');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
valid: issues.length === 0,
|
|
149
|
+
issues,
|
|
150
|
+
domain: license.domain,
|
|
151
|
+
license_id: license.license_id,
|
|
152
|
+
issued_to: license.issued_to,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function signLicense(license, privateKeyPem) {
|
|
157
|
+
const { signature: _, ...payload } = license;
|
|
158
|
+
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
159
|
+
// Use crypto.sign() for Ed25519 (supported via PEM keys)
|
|
160
|
+
const sig = crypto.sign(null, Buffer.from(data), privateKeyPem);
|
|
161
|
+
license.signature = `ed25519:${sig.toString('hex')}`;
|
|
162
|
+
return license;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function verifyLicenseSignature(license, publicKeyPem) {
|
|
166
|
+
const signature = license.signature?.replace('ed25519:', '') || '';
|
|
167
|
+
if (!signature) return false;
|
|
168
|
+
const { signature: _, ...payload } = license;
|
|
169
|
+
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
170
|
+
try {
|
|
171
|
+
return crypto.verify(null, Buffer.from(data), publicKeyPem, Buffer.from(signature, 'hex'));
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
encrypt,
|
|
179
|
+
decrypt,
|
|
180
|
+
deriveKey,
|
|
181
|
+
machineFingerprint,
|
|
182
|
+
isEncryptable,
|
|
183
|
+
isEncryptedContainer,
|
|
184
|
+
createLicense,
|
|
185
|
+
verifyLicense,
|
|
186
|
+
signLicense,
|
|
187
|
+
verifyLicenseSignature,
|
|
188
|
+
ENCRYPTED_FILES,
|
|
189
|
+
PLAINTEXT_FILES,
|
|
190
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KDNA License — Generate, verify, and manage domain licenses.
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* kdna license generate <domain> --to <email> [--expires <date>]
|
|
6
|
+
* kdna license verify <license.json>
|
|
7
|
+
* kdna license bind <license.json>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { EXIT, error } = require('./_common');
|
|
14
|
+
const { createLicense, signLicense, verifyLicense, verifyLicenseSignature, machineFingerprint } = require('./encrypt');
|
|
15
|
+
|
|
16
|
+
const IDENTITY_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna', 'identity');
|
|
17
|
+
const PRIVATE_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.key');
|
|
18
|
+
const PUBLIC_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.pub');
|
|
19
|
+
|
|
20
|
+
function readIdentity() {
|
|
21
|
+
if (!fs.existsSync(PRIVATE_KEY_PATH) || !fs.existsSync(PUBLIC_KEY_PATH)) {
|
|
22
|
+
error('No identity found. Run: kdna identity init', EXIT.INPUT_ERROR);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
privateKey: fs.readFileSync(PRIVATE_KEY_PATH, 'utf8'),
|
|
26
|
+
publicKey: fs.readFileSync(PUBLIC_KEY_PATH, 'utf8'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cmdLicenseGenerate(args) {
|
|
31
|
+
const domain = args[0];
|
|
32
|
+
if (!domain) error('Usage: kdna license generate <domain> --to <email> [--expires <date>] [--max-agents <n>]', EXIT.INPUT_ERROR);
|
|
33
|
+
|
|
34
|
+
const emailIdx = args.indexOf('--to');
|
|
35
|
+
const email = emailIdx >= 0 ? args[emailIdx + 1] : null;
|
|
36
|
+
if (!email) error('--to <email> is required', EXIT.INPUT_ERROR);
|
|
37
|
+
|
|
38
|
+
const expiresIdx = args.indexOf('--expires');
|
|
39
|
+
const expiresAt = expiresIdx >= 0 ? args[expiresIdx + 1] : null;
|
|
40
|
+
|
|
41
|
+
const agentsIdx = args.indexOf('--max-agents');
|
|
42
|
+
const maxAgents = agentsIdx >= 0 ? parseInt(args[agentsIdx + 1], 10) : 1;
|
|
43
|
+
|
|
44
|
+
const bindingIdx = args.includes('--no-binding');
|
|
45
|
+
const requireBinding = !bindingIdx;
|
|
46
|
+
|
|
47
|
+
const { privateKey, publicKey } = readIdentity();
|
|
48
|
+
|
|
49
|
+
const license = createLicense(domain, {
|
|
50
|
+
issuedTo: email,
|
|
51
|
+
expiresAt,
|
|
52
|
+
maxAgents,
|
|
53
|
+
requireMachineBinding: requireBinding,
|
|
54
|
+
allowedAgents: ['claude_code', 'codex', 'opencode', 'cursor', 'gemini'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const signed = signLicense(license, privateKey);
|
|
58
|
+
|
|
59
|
+
const pubkey = crypto.createHash('sha256').update(publicKey).digest('hex').substring(0, 12);
|
|
60
|
+
signed.scope_pubkey_fingerprint = `ed25519:${pubkey}`;
|
|
61
|
+
|
|
62
|
+
const saveIdx = args.indexOf('--save');
|
|
63
|
+
const savePath = saveIdx >= 0 ? args[saveIdx + 1] : null;
|
|
64
|
+
|
|
65
|
+
console.log(JSON.stringify(signed, null, 2));
|
|
66
|
+
|
|
67
|
+
if (savePath) {
|
|
68
|
+
fs.writeFileSync(savePath, JSON.stringify(signed, null, 2) + '\n');
|
|
69
|
+
console.error(`Saved to: ${savePath}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.error('');
|
|
73
|
+
console.error(`License generated for ${domain}`);
|
|
74
|
+
console.error(` Issued to: ${email}`);
|
|
75
|
+
console.error(` License ID: ${signed.license_id}`);
|
|
76
|
+
if (expiresAt) console.error(` Expires: ${expiresAt}`);
|
|
77
|
+
console.error(` Machine binding: ${requireBinding ? 'required' : 'disabled'}`);
|
|
78
|
+
console.error('');
|
|
79
|
+
console.error('Save this JSON as license.json inside the .kdnae container.');
|
|
80
|
+
console.error('Share the license key with the licensee.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cmdLicenseVerify(args) {
|
|
84
|
+
const licensePath = args[0];
|
|
85
|
+
if (!licensePath) error('Usage: kdna license verify <license.json>', EXIT.INPUT_ERROR);
|
|
86
|
+
|
|
87
|
+
let license;
|
|
88
|
+
try {
|
|
89
|
+
license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
90
|
+
} catch {
|
|
91
|
+
error(`Cannot read license file: ${licensePath}`, EXIT.INPUT_ERROR);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { publicKey } = readIdentity();
|
|
95
|
+
const signatureValid = verifyLicenseSignature(license, publicKey);
|
|
96
|
+
const fp = machineFingerprint();
|
|
97
|
+
const result = verifyLicense(license, publicKey, fp);
|
|
98
|
+
|
|
99
|
+
const jsonMode = args.includes('--json');
|
|
100
|
+
|
|
101
|
+
if (jsonMode) {
|
|
102
|
+
console.log(JSON.stringify({
|
|
103
|
+
domain: license.domain,
|
|
104
|
+
license_id: license.license_id,
|
|
105
|
+
issued_to: license.issued_to,
|
|
106
|
+
signature_valid: signatureValid,
|
|
107
|
+
valid: result.valid,
|
|
108
|
+
issues: result.issues,
|
|
109
|
+
fingerprint: license.require_machine_binding ? fp : 'not required',
|
|
110
|
+
}, null, 2));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(`License: ${license.license_id}`);
|
|
113
|
+
console.log(`Domain: ${license.domain}`);
|
|
114
|
+
console.log(`Issued: ${license.issued_to}`);
|
|
115
|
+
console.log(`Signature: ${signatureValid ? '✓ valid' : '✗ invalid'}`);
|
|
116
|
+
if (license.expires_at) {
|
|
117
|
+
const expired = new Date(license.expires_at) < new Date();
|
|
118
|
+
console.log(`Expires: ${license.expires_at} ${expired ? '(EXPIRED)' : ''}`);
|
|
119
|
+
}
|
|
120
|
+
if (license.require_machine_binding) {
|
|
121
|
+
console.log(`Machine binding: required`);
|
|
122
|
+
console.log(` Current fingerprint: ${fp}`);
|
|
123
|
+
}
|
|
124
|
+
if (result.issues.length) {
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log('Issues:');
|
|
127
|
+
result.issues.forEach(i => console.log(` ✗ ${i}`));
|
|
128
|
+
} else {
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log('✓ License valid');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.exit(result.valid && signatureValid ? EXIT.OK : EXIT.TRUST_FAILED);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cmdLicenseBind(args) {
|
|
138
|
+
const licensePath = args[0];
|
|
139
|
+
if (!licensePath) error('Usage: kdna license bind <license.json>', EXIT.INPUT_ERROR);
|
|
140
|
+
|
|
141
|
+
let license;
|
|
142
|
+
try {
|
|
143
|
+
license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
144
|
+
} catch {
|
|
145
|
+
error(`Cannot read license file: ${licensePath}`, EXIT.INPUT_ERROR);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!license.require_machine_binding) {
|
|
149
|
+
console.log('License does not require machine binding. No action needed.');
|
|
150
|
+
process.exit(EXIT.OK);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { privateKey, publicKey } = readIdentity();
|
|
154
|
+
const fp = machineFingerprint();
|
|
155
|
+
|
|
156
|
+
// Remove old signature before re-signing
|
|
157
|
+
delete license.signature;
|
|
158
|
+
license.machine_fingerprint = fp;
|
|
159
|
+
license.bound_at = new Date().toISOString();
|
|
160
|
+
|
|
161
|
+
const signed = signLicense(license, privateKey);
|
|
162
|
+
fs.writeFileSync(licensePath, JSON.stringify(signed, null, 2) + '\n');
|
|
163
|
+
console.log(`License bound to machine: ${fp}`);
|
|
164
|
+
console.log(`Updated: ${licensePath}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function cmdLicenseShow(args) {
|
|
168
|
+
const licensePath = args[0];
|
|
169
|
+
if (!licensePath) {
|
|
170
|
+
const local = path.join(process.cwd(), 'license.json');
|
|
171
|
+
if (fs.existsSync(local)) return cmdLicenseVerify([local, ...args.slice(1)]);
|
|
172
|
+
error('Usage: kdna license show <license.json>', EXIT.INPUT_ERROR);
|
|
173
|
+
}
|
|
174
|
+
cmdLicenseVerify(args);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cmdLicenseInstall(args) {
|
|
178
|
+
const licensePath = args[0];
|
|
179
|
+
if (!licensePath) error('Usage: kdna license install <license.json>', EXIT.INPUT_ERROR);
|
|
180
|
+
|
|
181
|
+
let license;
|
|
182
|
+
try {
|
|
183
|
+
license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
184
|
+
} catch {
|
|
185
|
+
error(`Cannot read license file: ${licensePath}`, EXIT.INPUT_ERROR);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!license.domain) error('License missing domain field', EXIT.INPUT_ERROR);
|
|
189
|
+
|
|
190
|
+
const licenseDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna', 'licenses');
|
|
191
|
+
fs.mkdirSync(licenseDir, { recursive: true });
|
|
192
|
+
|
|
193
|
+
const safeName = license.domain.replace(/^@/, '').replace('/', '-');
|
|
194
|
+
const dest = path.join(licenseDir, `${safeName}.json`);
|
|
195
|
+
|
|
196
|
+
fs.writeFileSync(dest, JSON.stringify(license, null, 2) + '\n');
|
|
197
|
+
|
|
198
|
+
console.log(`License installed for ${license.domain}`);
|
|
199
|
+
console.log(` License ID: ${license.license_id || 'unknown'}`);
|
|
200
|
+
console.log(` Saved to: ${dest}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(`Now install the domain: kdna install ${license.domain}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow, cmdLicenseInstall };
|
package/src/install.js
CHANGED
|
@@ -20,6 +20,7 @@ const crypto = require('crypto');
|
|
|
20
20
|
const { execSync, execFileSync } = require('child_process');
|
|
21
21
|
const { RegistryResolver, parseName } = require('./registry');
|
|
22
22
|
const { EXIT, error } = require('./cmds/_common');
|
|
23
|
+
const { decrypt, deriveKey, machineFingerprint, isEncryptedContainer, ENCRYPTED_FILES } = require('./cmds/encrypt');
|
|
23
24
|
|
|
24
25
|
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
25
26
|
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
@@ -199,9 +200,9 @@ function warnLegacy() {
|
|
|
199
200
|
// ─── Source parsing ─────────────────────────────────────────────────────
|
|
200
201
|
|
|
201
202
|
function parseSource(input) {
|
|
202
|
-
// Local file
|
|
203
|
+
// Local file (.kdna or .kdnae)
|
|
203
204
|
if (
|
|
204
|
-
input.endsWith('.kdna') &&
|
|
205
|
+
(input.endsWith('.kdna') || input.endsWith('.kdnae')) &&
|
|
205
206
|
(input.startsWith('./') || input.startsWith('/') || input.startsWith('~/'))
|
|
206
207
|
) {
|
|
207
208
|
const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
|
|
@@ -283,6 +284,53 @@ print('ok')
|
|
|
283
284
|
}
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
function extractAndDecrypt(kdnaPath, destDir, licenseKey) {
|
|
288
|
+
extractKdna(kdnaPath, destDir);
|
|
289
|
+
const fp = machineFingerprint();
|
|
290
|
+
const key = deriveKey(licenseKey, fp);
|
|
291
|
+
|
|
292
|
+
for (const f of fs.readdirSync(destDir)) {
|
|
293
|
+
if (ENCRYPTED_FILES.includes(f)) {
|
|
294
|
+
try {
|
|
295
|
+
const fullPath = path.join(destDir, f);
|
|
296
|
+
const encrypted = fs.readFileSync(fullPath);
|
|
297
|
+
const decrypted = decrypt(encrypted, key);
|
|
298
|
+
fs.writeFileSync(fullPath, decrypted);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
301
|
+
error(`Failed to decrypt ${f}: ${err.message}. Wrong license key?`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function findLicense(domainName) {
|
|
308
|
+
const licenseDir = path.join(USER_KDNA_DIR, 'licenses');
|
|
309
|
+
const licensePath = path.join(licenseDir, `${domainName.replace(/^@/, '').replace('/', '-')}.json`);
|
|
310
|
+
if (fs.existsSync(licensePath)) {
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
313
|
+
} catch { /* invalid license */ }
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function findLicenseForDomain(domainFull) {
|
|
319
|
+
const licenseDir = path.join(USER_KDNA_DIR, 'licenses');
|
|
320
|
+
if (!fs.existsSync(licenseDir)) return null;
|
|
321
|
+
// Try exact match: @aikdna/writing → @aikdna-writing.json
|
|
322
|
+
const candidates = [domainFull.replace(/^@/, '').replace('/', '-')];
|
|
323
|
+
// Also try just the domain name
|
|
324
|
+
candidates.push(domainFull.split('/').pop());
|
|
325
|
+
for (const c of candidates) {
|
|
326
|
+
const p = path.join(licenseDir, `${c}.json`);
|
|
327
|
+
if (fs.existsSync(p)) {
|
|
328
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { /* skip */ }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
286
334
|
// ─── Signature verification ────────────────────────────────────────────
|
|
287
335
|
|
|
288
336
|
function verifySignature({ destDir, scope, entry, lenient = true }) {
|
|
@@ -613,9 +661,53 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
|
|
|
613
661
|
const abs = path.resolve(filePath);
|
|
614
662
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) error(`Not a file: ${abs}`);
|
|
615
663
|
|
|
664
|
+
const isEncrypted = isEncryptedContainer(abs);
|
|
616
665
|
const tmpDir = path.join(INSTALL_DIR, '.local-tmp-' + Date.now());
|
|
617
666
|
ensureDir(tmpDir);
|
|
618
|
-
|
|
667
|
+
|
|
668
|
+
if (isEncrypted) {
|
|
669
|
+
// Find license for this .kdnae file
|
|
670
|
+
// First check the license directory, then ask for --license flag from args
|
|
671
|
+
const licenseFromArgs = process.argv.includes('--license')
|
|
672
|
+
? process.argv[process.argv.indexOf('--license') + 1]
|
|
673
|
+
: null;
|
|
674
|
+
let licenseKey = null;
|
|
675
|
+
|
|
676
|
+
if (licenseFromArgs && fs.existsSync(licenseFromArgs)) {
|
|
677
|
+
try {
|
|
678
|
+
const lic = JSON.parse(fs.readFileSync(licenseFromArgs, 'utf8'));
|
|
679
|
+
licenseKey = lic.license_id;
|
|
680
|
+
} catch { /* invalid license file */ }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (!licenseKey) {
|
|
684
|
+
// Try to auto-discover from ~/.kdna/licenses/
|
|
685
|
+
const manifest = readJson(path.join(tmpDir, 'kdna.json'));
|
|
686
|
+
// We need to extract just the manifest first to get the domain name
|
|
687
|
+
extractKdna(abs, tmpDir);
|
|
688
|
+
const mf = readJson(path.join(tmpDir, 'kdna.json'));
|
|
689
|
+
if (mf?.name) {
|
|
690
|
+
const lic = findLicenseForDomain(mf.name);
|
|
691
|
+
if (lic) licenseKey = lic.license_id;
|
|
692
|
+
}
|
|
693
|
+
if (!licenseKey) {
|
|
694
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
695
|
+
error(
|
|
696
|
+
`Cannot install encrypted .kdnae without a license.\n` +
|
|
697
|
+
`Save the license to ~/.kdna/licenses/ or use --license <file>.`,
|
|
698
|
+
EXIT.TRUST_FAILED,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
// Re-extract for decryption
|
|
702
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
703
|
+
ensureDir(tmpDir);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
console.log(' Decrypting .kdnae container...');
|
|
707
|
+
extractAndDecrypt(abs, tmpDir, licenseKey);
|
|
708
|
+
} else {
|
|
709
|
+
extractKdna(abs, tmpDir);
|
|
710
|
+
}
|
|
619
711
|
|
|
620
712
|
const manifest = readJson(path.join(tmpDir, 'kdna.json'));
|
|
621
713
|
const declared = manifest?.name;
|
|
@@ -636,6 +728,7 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
|
|
|
636
728
|
destManifest._source = {
|
|
637
729
|
type: 'local-file',
|
|
638
730
|
path: abs,
|
|
731
|
+
encrypted: isEncrypted,
|
|
639
732
|
installed_at: new Date().toISOString(),
|
|
640
733
|
};
|
|
641
734
|
fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
|
|
@@ -647,9 +740,10 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
|
|
|
647
740
|
path: dest,
|
|
648
741
|
source: 'local-file',
|
|
649
742
|
source_path: abs,
|
|
743
|
+
encrypted: isEncrypted,
|
|
650
744
|
}));
|
|
651
745
|
} else {
|
|
652
|
-
console.log(`✓ Installed ${declared} from local file`);
|
|
746
|
+
console.log(`✓ Installed ${declared} from ${isEncrypted ? 'encrypted' : 'local'} file`);
|
|
653
747
|
console.log(` Location: ${dest}`);
|
|
654
748
|
}
|
|
655
749
|
}
|