@aikdna/kdna-cli 0.14.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 +5 -1
- package/src/cmds/license.js +29 -2
- package/src/install.js +98 -4
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -24,7 +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 } = require('./cmds/license');
|
|
27
|
+
const { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow, cmdLicenseInstall } = require('./cmds/license');
|
|
28
28
|
const { cmdPreview, cmdProject, cmdEval, cmdExport, cmdDemo } = require('./cmds/legacy');
|
|
29
29
|
const { cmdStudioScaffold, cmdCardsValidate, cmdLockVerify, cmdStudioCompile, cmdStudioReadiness } = require('./cmds/studio');
|
|
30
30
|
const { cmdTestRun, cmdTestImport } = require('./cmds/test');
|
|
@@ -138,6 +138,7 @@ Trace & Diagnostics:
|
|
|
138
138
|
|
|
139
139
|
License & Authorization:
|
|
140
140
|
license generate <domain> --to <email> Generate signed license
|
|
141
|
+
license install <license.json> Register license for auto-decrypt
|
|
141
142
|
license verify <license.json> Verify license signature
|
|
142
143
|
license bind <license.json> Bind license to this machine
|
|
143
144
|
license show <license.json> Display license details
|
|
@@ -436,10 +437,13 @@ switch (cmd) {
|
|
|
436
437
|
cmdLicenseBind(rest);
|
|
437
438
|
} else if (sub === 'show') {
|
|
438
439
|
cmdLicenseShow(rest);
|
|
440
|
+
} else if (sub === 'install') {
|
|
441
|
+
cmdLicenseInstall(rest);
|
|
439
442
|
} else {
|
|
440
443
|
error(
|
|
441
444
|
'Usage:\n' +
|
|
442
445
|
' kdna license generate <domain> --to <email> [--expires <date>]\n' +
|
|
446
|
+
' kdna license install <license.json>\n' +
|
|
443
447
|
' kdna license verify <license.json>\n' +
|
|
444
448
|
' kdna license bind <license.json>\n' +
|
|
445
449
|
' kdna license show <license.json>',
|
package/src/cmds/license.js
CHANGED
|
@@ -167,7 +167,6 @@ function cmdLicenseBind(args) {
|
|
|
167
167
|
function cmdLicenseShow(args) {
|
|
168
168
|
const licensePath = args[0];
|
|
169
169
|
if (!licensePath) {
|
|
170
|
-
// Try to find license in current directory
|
|
171
170
|
const local = path.join(process.cwd(), 'license.json');
|
|
172
171
|
if (fs.existsSync(local)) return cmdLicenseVerify([local, ...args.slice(1)]);
|
|
173
172
|
error('Usage: kdna license show <license.json>', EXIT.INPUT_ERROR);
|
|
@@ -175,4 +174,32 @@ function cmdLicenseShow(args) {
|
|
|
175
174
|
cmdLicenseVerify(args);
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
|
|
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
|
}
|