@aikdna/kdna-core 0.15.11 → 0.16.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/CHANGELOG.md +60 -0
- package/README.md +36 -21
- package/package.json +2 -7
- package/schema/kdna-envelope-external-grant-v1.schema.json +20 -0
- package/schema/kdna-key-grant-v1.schema.json +54 -0
- package/schema/load-plan.schema.json +1 -1
- package/schema/manifest.schema.json +3 -3
- package/src/asset-reader.js +7 -11
- package/src/container-dispatcher.js +24 -102
- package/src/crypto-profile.js +6 -3
- package/src/external-key-grant.js +560 -0
- package/src/index.js +4 -0
- package/src/index.mjs +39 -0
- package/src/lint-pure.js +8 -13
- package/src/runtime-api.js +91 -0
- package/src/types.d.ts +174 -19
- package/src/v1/index.js +245 -95
- package/src/v1/index.mjs +7 -8
package/src/v1/index.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* kdna monorepo CLI shim.
|
|
2
|
+
* KDNA Asset Container inspect / validate / pack / unpack implementation.
|
|
4
3
|
*
|
|
5
4
|
* KDNA Core is the official KDNA judgment-asset format and runtime
|
|
6
5
|
* loading contract. .kdna assets are created, inspected, packed,
|
|
7
6
|
* unpacked, and validated through the official KDNA toolchain. This
|
|
8
|
-
* module is the v1 component of that toolchain.
|
|
9
7
|
*
|
|
10
|
-
* The KDNA
|
|
8
|
+
* The KDNA Asset Container is documented in docs/core/file-format.md.
|
|
11
9
|
* This module is the shared implementation that:
|
|
12
10
|
*
|
|
13
|
-
* - packages/kdna/bin/kdna.js uses as
|
|
14
|
-
* - scripts
|
|
11
|
+
* - packages/kdna/bin/kdna.js uses as its container router
|
|
12
|
+
* - compatibility scripts delegate to it so historical tooling
|
|
15
13
|
* scripts and the official CLI cannot drift
|
|
16
14
|
*
|
|
17
15
|
* Hard rules from the format spec:
|
|
@@ -38,24 +36,29 @@ const fs = require('node:fs');
|
|
|
38
36
|
const path = require('node:path');
|
|
39
37
|
const zlib = require('node:zlib');
|
|
40
38
|
const crypto = require('node:crypto');
|
|
39
|
+
const cbor = require('cbor-x');
|
|
40
|
+
const {
|
|
41
|
+
EXTERNAL_ENVELOPE_PROFILE,
|
|
42
|
+
validateExternalEnvelope,
|
|
43
|
+
isVerifiedExternalEntitlement,
|
|
44
|
+
} = require('../external-key-grant');
|
|
41
45
|
|
|
42
46
|
const { readAsset } = (() => {
|
|
43
47
|
try {
|
|
44
48
|
return require('../container-dispatcher.js');
|
|
45
49
|
} catch {
|
|
46
|
-
return { readAsset: null
|
|
50
|
+
return { readAsset: null };
|
|
47
51
|
}
|
|
48
52
|
})();
|
|
49
53
|
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
...
|
|
56
|
-
...V1_OPTIONAL_DIR_ENTRIES,
|
|
54
|
+
const MIMETYPE = 'application/vnd.kdna.asset';
|
|
55
|
+
const REQUIRED_DIR_ENTRIES = ['mimetype', 'kdna.json', 'payload.kdnab'];
|
|
56
|
+
const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'signatures', 'attachments'];
|
|
57
|
+
const ALLOWED_TOP_LEVEL_ENTRIES = new Set([
|
|
58
|
+
...REQUIRED_DIR_ENTRIES,
|
|
59
|
+
...OPTIONAL_DIR_ENTRIES,
|
|
57
60
|
]);
|
|
58
|
-
const
|
|
61
|
+
const FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
|
|
59
62
|
'KDNA_Core.json',
|
|
60
63
|
'KDNA_Patterns.json',
|
|
61
64
|
'KDNA_Scenarios.json',
|
|
@@ -64,6 +67,12 @@ const V1_FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
|
|
|
64
67
|
'KDNA_Evolution.json',
|
|
65
68
|
]);
|
|
66
69
|
|
|
70
|
+
// Legacy aliases — prefer the unversioned names above
|
|
71
|
+
const V1_REQUIRED_DIR_ENTRIES = REQUIRED_DIR_ENTRIES;
|
|
72
|
+
const V1_OPTIONAL_DIR_ENTRIES = OPTIONAL_DIR_ENTRIES;
|
|
73
|
+
const V1_ALLOWED_TOP_LEVEL_ENTRIES = ALLOWED_TOP_LEVEL_ENTRIES;
|
|
74
|
+
const V1_FORBIDDEN_LEGACY_TOP_LEVEL = FORBIDDEN_LEGACY_TOP_LEVEL;
|
|
75
|
+
|
|
67
76
|
const DEFAULT_CONTAINER_LIMITS = Object.freeze({
|
|
68
77
|
maxContainerBytes: 25 * 1024 * 1024,
|
|
69
78
|
maxEntries: 128,
|
|
@@ -172,49 +181,32 @@ function loadSchemas() {
|
|
|
172
181
|
// ─── Format detection ──────────────────────────────────────────────────
|
|
173
182
|
|
|
174
183
|
/**
|
|
175
|
-
* Detect whether a directory is a
|
|
184
|
+
* Detect whether a directory is a KDNA authoring source layout.
|
|
176
185
|
* Required entries: mimetype, kdna.json, payload.kdnab.
|
|
177
186
|
* mimetype content must equal "application/vnd.kdna.asset".
|
|
178
187
|
*/
|
|
179
|
-
function
|
|
188
|
+
function isKdnaSourceDir(absPath) {
|
|
180
189
|
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) return false;
|
|
181
190
|
for (const f of V1_REQUIRED_DIR_ENTRIES) {
|
|
182
191
|
if (!fs.existsSync(path.join(absPath, f))) return false;
|
|
183
192
|
}
|
|
184
193
|
const mime = fs.readFileSync(path.join(absPath, 'mimetype'), 'utf8');
|
|
185
|
-
return mime ===
|
|
194
|
+
return mime === MIMETYPE;
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
/**
|
|
189
|
-
* Detect whether a directory is a v2 source layout.
|
|
190
|
-
* Required entries: mimetype, kdna.json, payload.kdnab.
|
|
191
|
-
* mimetype content must equal "application/vnd.aikdna.kdna+zip".
|
|
192
|
-
*/
|
|
193
|
-
function isV2SourceDir(absPath) {
|
|
194
|
-
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) return false;
|
|
195
|
-
for (const f of V1_REQUIRED_DIR_ENTRIES) {
|
|
196
|
-
if (!fs.existsSync(path.join(absPath, f))) return false;
|
|
197
|
-
}
|
|
198
|
-
const mime = fs.readFileSync(path.join(absPath, 'mimetype'), 'utf8');
|
|
199
|
-
return mime === MIMETYPE_V2;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
198
|
/**
|
|
203
|
-
* Detect whether a file is a
|
|
204
|
-
* Returns '
|
|
199
|
+
* Detect whether a file is a .kdna container.
|
|
200
|
+
* Returns 'kdna' | null. null = not a .kdna file or unreadable.
|
|
205
201
|
*/
|
|
206
202
|
function detectContainerFormat(absPath) {
|
|
207
203
|
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) return null;
|
|
208
|
-
// Quick header check: must look like a ZIP.
|
|
209
204
|
const fd = fs.openSync(absPath, 'r');
|
|
210
205
|
const head = Buffer.alloc(4);
|
|
211
206
|
fs.readSync(fd, head, 0, 4, 0);
|
|
212
207
|
fs.closeSync(fd);
|
|
213
208
|
if (head[0] !== 0x50 || head[1] !== 0x4b) return null;
|
|
214
209
|
|
|
215
|
-
// Read only the first central-directory entry. Dangerous later entries
|
|
216
|
-
// must not make detection fall through to a less strict legacy route;
|
|
217
|
-
// readV1Layout/validate will reject them with the secure container reader.
|
|
218
210
|
let first;
|
|
219
211
|
try {
|
|
220
212
|
first = readFirstZipEntry(absPath);
|
|
@@ -222,12 +214,9 @@ function detectContainerFormat(absPath) {
|
|
|
222
214
|
return null;
|
|
223
215
|
}
|
|
224
216
|
if (first.name !== 'mimetype') return null;
|
|
225
|
-
// The mimetype entry must be STORED (method 0).
|
|
226
217
|
if (first.method !== 0) return null;
|
|
227
218
|
const mime = first.method === 0 ? first.data.toString('utf8') : '';
|
|
228
|
-
|
|
229
|
-
if (mime === MIMETYPE_V2) return 'v2';
|
|
230
|
-
return null;
|
|
219
|
+
return (mime === MIMETYPE) ? 'kdna' : null;
|
|
231
220
|
}
|
|
232
221
|
|
|
233
222
|
// ─── ZIP I/O ────────────────────────────────────────────────────────────
|
|
@@ -550,10 +539,10 @@ function readV1Layout(absPath) {
|
|
|
550
539
|
for (const f of V1_REQUIRED_DIR_ENTRIES) {
|
|
551
540
|
const full = path.join(absPath, f);
|
|
552
541
|
if (!fs.existsSync(full)) {
|
|
553
|
-
throw new Error(`not a KDNA
|
|
542
|
+
throw new Error(`not a KDNA authoring source directory: missing ${f}`);
|
|
554
543
|
}
|
|
555
544
|
if (fs.lstatSync(full).isSymbolicLink()) {
|
|
556
|
-
throw new Error(`not a KDNA
|
|
545
|
+
throw new Error(`not a KDNA authoring source directory: ${f} must not be a symlink`);
|
|
557
546
|
}
|
|
558
547
|
}
|
|
559
548
|
for (const f of [...V1_REQUIRED_DIR_ENTRIES, ...V1_OPTIONAL_DIR_ENTRIES]) {
|
|
@@ -571,10 +560,10 @@ function readV1Layout(absPath) {
|
|
|
571
560
|
kind = 'file';
|
|
572
561
|
entries = listZipEntries(absPath);
|
|
573
562
|
if (entries.length === 0 || entries[0].name !== 'mimetype') {
|
|
574
|
-
throw new Error('not a KDNA
|
|
563
|
+
throw new Error('not a KDNA Asset Container: first entry is not mimetype');
|
|
575
564
|
}
|
|
576
565
|
if (entries[0].method !== 0) {
|
|
577
|
-
throw new Error('not a KDNA
|
|
566
|
+
throw new Error('not a KDNA Asset Container: mimetype must be uncompressed');
|
|
578
567
|
}
|
|
579
568
|
for (const e of entries) {
|
|
580
569
|
// We only need the well-known entries; signatures/ attachments/ etc.
|
|
@@ -590,7 +579,7 @@ function readV1Layout(absPath) {
|
|
|
590
579
|
}
|
|
591
580
|
for (const f of V1_REQUIRED_DIR_ENTRIES) {
|
|
592
581
|
if (!map[f]) {
|
|
593
|
-
throw new Error(`not a KDNA
|
|
582
|
+
throw new Error(`not a KDNA Asset Container: missing ${f}`);
|
|
594
583
|
}
|
|
595
584
|
}
|
|
596
585
|
} else {
|
|
@@ -599,9 +588,9 @@ function readV1Layout(absPath) {
|
|
|
599
588
|
|
|
600
589
|
// mimetype content must equal the literal v1 media type.
|
|
601
590
|
const mime = map.mimetype.toString('utf8');
|
|
602
|
-
if (mime !==
|
|
591
|
+
if (mime !== MIMETYPE) {
|
|
603
592
|
throw new Error(
|
|
604
|
-
`not a KDNA layout: mimetype is "${mime}", expected "${
|
|
593
|
+
`not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE}"`,
|
|
605
594
|
);
|
|
606
595
|
}
|
|
607
596
|
|
|
@@ -657,6 +646,24 @@ function assertJsonWithinLimits(value, name, limits) {
|
|
|
657
646
|
walk(value, 0);
|
|
658
647
|
}
|
|
659
648
|
|
|
649
|
+
function parsePayloadEntry(name, bytes, opts = {}) {
|
|
650
|
+
if (!Buffer.isBuffer(bytes)) {
|
|
651
|
+
throw new Error(`${name} is not a file entry`);
|
|
652
|
+
}
|
|
653
|
+
const limits = { ...DEFAULT_CONTAINER_LIMITS, ...(opts.limits || {}) };
|
|
654
|
+
if (bytes.length > limits.maxEntryBytes) {
|
|
655
|
+
throw new Error(`${name} exceeds maximum size (${limits.maxEntryBytes} bytes)`);
|
|
656
|
+
}
|
|
657
|
+
let parsed;
|
|
658
|
+
try {
|
|
659
|
+
parsed = cbor.decode(bytes);
|
|
660
|
+
} catch {
|
|
661
|
+
throw new Error(`${name}: not valid CBOR. payload.kdnab must be CBOR-encoded (kdna_version 1.0).`);
|
|
662
|
+
}
|
|
663
|
+
assertJsonWithinLimits(parsed, name, limits);
|
|
664
|
+
return parsed;
|
|
665
|
+
}
|
|
666
|
+
|
|
660
667
|
// ─── inspect ───────────────────────────────────────────────────────────
|
|
661
668
|
|
|
662
669
|
/**
|
|
@@ -710,9 +717,9 @@ function runValidate(v1) {
|
|
|
710
717
|
}
|
|
711
718
|
if (v1.map.mimetype) {
|
|
712
719
|
const mimeStr = v1.map.mimetype.toString('utf8');
|
|
713
|
-
if (mimeStr !==
|
|
720
|
+
if (mimeStr !== MIMETYPE) {
|
|
714
721
|
result.format_valid = false;
|
|
715
|
-
problems.push(`format: mimetype is not ${
|
|
722
|
+
problems.push(`format: mimetype is not ${MIMETYPE}`);
|
|
716
723
|
}
|
|
717
724
|
}
|
|
718
725
|
|
|
@@ -721,7 +728,7 @@ function runValidate(v1) {
|
|
|
721
728
|
if (!validators) {
|
|
722
729
|
result.schema_valid = false;
|
|
723
730
|
problems.push(
|
|
724
|
-
'schema:
|
|
731
|
+
'schema: KDNA Core requires ajv and ajv-formats for JSON-Schema validation. Run: npm install ajv ajv-formats (or: npm install -g ajv ajv-formats for global CLI users)',
|
|
725
732
|
);
|
|
726
733
|
return finalizeValidate(result, problems);
|
|
727
734
|
}
|
|
@@ -736,10 +743,10 @@ function runValidate(v1) {
|
|
|
736
743
|
// For actually encrypted payloads (envelopes), skip schema validation.
|
|
737
744
|
let payload;
|
|
738
745
|
try {
|
|
739
|
-
payload =
|
|
746
|
+
payload = parsePayloadEntry("payload.kdnab", v1.map['payload.kdnab']);
|
|
740
747
|
} catch (e) {
|
|
741
748
|
result.payload_valid = false;
|
|
742
|
-
problems.push(`payload: not valid
|
|
749
|
+
problems.push(`payload: not valid KDNA payload (${e.message})`);
|
|
743
750
|
return finalizeValidate(result, problems);
|
|
744
751
|
}
|
|
745
752
|
|
|
@@ -754,7 +761,16 @@ function runValidate(v1) {
|
|
|
754
761
|
result.payload_valid = false;
|
|
755
762
|
problems.push(`payload: encrypted envelope profile ${payload.profile || 'unknown'} does not match manifest encryption profile ${encProfile}`);
|
|
756
763
|
}
|
|
757
|
-
|
|
764
|
+
let hasKeyMaterial = !!payload.wrapped_key || (Array.isArray(payload.key_slots) && payload.key_slots.length > 0);
|
|
765
|
+
if (payload.profile === EXTERNAL_ENVELOPE_PROFILE) {
|
|
766
|
+
try {
|
|
767
|
+
validateExternalEnvelope(payload);
|
|
768
|
+
hasKeyMaterial = true;
|
|
769
|
+
} catch (error) {
|
|
770
|
+
result.payload_valid = false;
|
|
771
|
+
problems.push(`payload: ${error.message}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
758
774
|
if (!payload.profile || !payload.ciphertext || !hasKeyMaterial) {
|
|
759
775
|
result.payload_valid = false;
|
|
760
776
|
problems.push('payload: encrypted envelope missing required fields (profile/ciphertext/key material)');
|
|
@@ -915,7 +931,7 @@ function buildChecksumsV1(sourceDir) {
|
|
|
915
931
|
// ─── pack ──────────────────────────────────────────────────────────────
|
|
916
932
|
|
|
917
933
|
/**
|
|
918
|
-
* Pack
|
|
934
|
+
* Pack an authoring source directory into a .kdna asset. Output is
|
|
919
935
|
* deterministic: the same source directory packed twice produces
|
|
920
936
|
* byte-identical output (fixed DOS timestamps, fixed entry order,
|
|
921
937
|
* mimetype first).
|
|
@@ -931,12 +947,22 @@ function pack(sourceDir, outputPath) {
|
|
|
931
947
|
}
|
|
932
948
|
}
|
|
933
949
|
const mime = fs.readFileSync(path.join(absSrc, 'mimetype'), 'utf8');
|
|
934
|
-
if (mime !==
|
|
935
|
-
throw new Error(`cannot pack: mimetype is "${mime}", expected "${
|
|
950
|
+
if (mime !== MIMETYPE) {
|
|
951
|
+
throw new Error(`cannot pack: mimetype is "${mime}", expected "${MIMETYPE}"`);
|
|
936
952
|
}
|
|
937
953
|
|
|
938
954
|
// Collect deterministically; mimetype is forced first.
|
|
939
955
|
const collected = listSourceDir(absSrc);
|
|
956
|
+
for (const entry of collected) {
|
|
957
|
+
const normalizedName = normalizeContainerEntryName(entry.rel);
|
|
958
|
+
const topLevel = normalizedName.split('/')[0];
|
|
959
|
+
if (!ALLOWED_TOP_LEVEL_ENTRIES.has(topLevel)) {
|
|
960
|
+
if (FORBIDDEN_LEGACY_TOP_LEVEL.has(topLevel)) {
|
|
961
|
+
throw new Error(`cannot pack forbidden top-level source entry: ${topLevel}`);
|
|
962
|
+
}
|
|
963
|
+
throw new Error(`cannot pack unsupported top-level entry: ${topLevel}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
940
966
|
const order = ['mimetype', ...collected.map((e) => e.rel).filter((n) => n !== 'mimetype')];
|
|
941
967
|
|
|
942
968
|
// Build the ZIP body.
|
|
@@ -946,7 +972,7 @@ function pack(sourceDir, outputPath) {
|
|
|
946
972
|
for (const rel of order) {
|
|
947
973
|
let data;
|
|
948
974
|
if (rel === 'mimetype') {
|
|
949
|
-
data = Buffer.from(
|
|
975
|
+
data = Buffer.from(MIMETYPE, 'utf8');
|
|
950
976
|
} else {
|
|
951
977
|
const found = collected.find((e) => e.rel === rel);
|
|
952
978
|
if (!found) continue;
|
|
@@ -1003,7 +1029,7 @@ function unpack(inputPath, outputDir) {
|
|
|
1003
1029
|
throw new Error(`not a file: ${absIn}`);
|
|
1004
1030
|
}
|
|
1005
1031
|
const entries = listZipEntries(absIn);
|
|
1006
|
-
// Sanity:
|
|
1032
|
+
// Sanity: the asset must have mimetype as its first, uncompressed entry.
|
|
1007
1033
|
if (entries.length === 0 || entries[0].name !== 'mimetype') {
|
|
1008
1034
|
throw new Error('not a KDNA container: first entry is not mimetype');
|
|
1009
1035
|
}
|
|
@@ -1011,9 +1037,9 @@ function unpack(inputPath, outputDir) {
|
|
|
1011
1037
|
throw new Error('not a KDNA container: mimetype must be uncompressed');
|
|
1012
1038
|
}
|
|
1013
1039
|
const mime = entries[0].data.toString('utf8');
|
|
1014
|
-
if (mime !==
|
|
1040
|
+
if (mime !== MIMETYPE) {
|
|
1015
1041
|
throw new Error(
|
|
1016
|
-
`not a KDNA container: mimetype is "${mime}", expected "${
|
|
1042
|
+
`not a KDNA container: mimetype is "${mime}", expected "${MIMETYPE}"`,
|
|
1017
1043
|
);
|
|
1018
1044
|
}
|
|
1019
1045
|
const absOut = path.resolve(outputDir);
|
|
@@ -1087,12 +1113,11 @@ function finalizeLoadPlan(plan) {
|
|
|
1087
1113
|
return plan;
|
|
1088
1114
|
}
|
|
1089
1115
|
|
|
1090
|
-
function computeSourceFingerprint(
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1116
|
+
function computeSourceFingerprint(_inputPath, v1) {
|
|
1117
|
+
// Fingerprint the logical entries, not the transport ZIP bytes. DEFLATE
|
|
1118
|
+
// output can differ between zlib versions and operating systems even when
|
|
1119
|
+
// every uncompressed KDNA entry is identical. LoadPlan conformance must be
|
|
1120
|
+
// stable across JS, Swift, CI runners, and repackaging transports.
|
|
1096
1121
|
const hash = crypto.createHash('sha256');
|
|
1097
1122
|
for (const name of Object.keys(v1.map).sort()) {
|
|
1098
1123
|
const bytes = v1.map[name];
|
|
@@ -1197,14 +1222,15 @@ function canonicalToV1Layout(asset) {
|
|
|
1197
1222
|
}
|
|
1198
1223
|
|
|
1199
1224
|
/**
|
|
1200
|
-
* Plan a KDNA
|
|
1225
|
+
* Plan a KDNA runtime load without decrypting or emitting judgment content.
|
|
1201
1226
|
* Product consumers such as Chat should render authorization UI from this
|
|
1202
1227
|
* result instead of parsing manifest fields directly.
|
|
1203
1228
|
*/
|
|
1204
1229
|
function planLoad(inputPath, opts = {}) {
|
|
1205
1230
|
let v1;
|
|
1206
1231
|
|
|
1207
|
-
// Try the unified container dispatcher first
|
|
1232
|
+
// Try the unified container dispatcher first; the internal compatibility
|
|
1233
|
+
// reader below remains available only for authoring and migration tooling.
|
|
1208
1234
|
if (readAsset !== null) {
|
|
1209
1235
|
try {
|
|
1210
1236
|
const asset = readAsset(path.resolve(inputPath));
|
|
@@ -1264,6 +1290,36 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1264
1290
|
|
|
1265
1291
|
const manifest = v1.manifest;
|
|
1266
1292
|
|
|
1293
|
+
function externalEntitlementMatchesCurrentAsset(entitlement) {
|
|
1294
|
+
if (!isVerifiedExternalEntitlement(entitlement) || !entitlement.asset) return false;
|
|
1295
|
+
let checksums;
|
|
1296
|
+
try {
|
|
1297
|
+
checksums = v1.map['checksums.json']
|
|
1298
|
+
? parseJsonEntry('checksums.json', v1.map['checksums.json'])
|
|
1299
|
+
: null;
|
|
1300
|
+
} catch {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
return entitlement.asset.asset_id === (manifest.asset_id || manifest.name)
|
|
1304
|
+
&& entitlement.asset.asset_uid === manifest.asset_uid
|
|
1305
|
+
&& entitlement.asset.version === manifest.version
|
|
1306
|
+
&& entitlement.asset.digest === checksums?.asset_digest
|
|
1307
|
+
&& entitlement.asset.entry_path === manifest.payload?.path;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function rejectExternalEntitlementMismatch() {
|
|
1311
|
+
plan.state = 'invalid';
|
|
1312
|
+
plan.required_action = 'block';
|
|
1313
|
+
plan.can_load_now = false;
|
|
1314
|
+
plan.projection_policy = 'none';
|
|
1315
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1316
|
+
'KDNA_GRANT_ASSET_MISMATCH',
|
|
1317
|
+
'blocking',
|
|
1318
|
+
'The verified account/device grant is bound to a different asset, version, digest, or encrypted entry.',
|
|
1319
|
+
));
|
|
1320
|
+
return finalizeLoadPlan(plan);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1267
1323
|
// Resolve dependencies if declared (Story 6)
|
|
1268
1324
|
const resolvedDeps = [];
|
|
1269
1325
|
if (manifest.dependencies && typeof manifest.dependencies === 'object' && Object.keys(manifest.dependencies).length > 0) {
|
|
@@ -1435,6 +1491,23 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1435
1491
|
}
|
|
1436
1492
|
|
|
1437
1493
|
if (plan.entitlement_profile === 'account') {
|
|
1494
|
+
if (isVerifiedExternalEntitlement(opts.entitlement)) {
|
|
1495
|
+
if (!externalEntitlementMatchesCurrentAsset(opts.entitlement)) {
|
|
1496
|
+
return rejectExternalEntitlementMismatch();
|
|
1497
|
+
}
|
|
1498
|
+
plan.state = opts.entitlement.status === 'offline_grace' ? 'offline_grace' : 'ready';
|
|
1499
|
+
plan.required_action = opts.entitlement.status === 'offline_grace' ? 'sync' : 'load';
|
|
1500
|
+
plan.can_load_now = true;
|
|
1501
|
+
plan.projection_policy = 'minimal';
|
|
1502
|
+
if (opts.entitlement.status === 'offline_grace') {
|
|
1503
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1504
|
+
'KDNA_AUTH_OFFLINE_GRACE_ACTIVE',
|
|
1505
|
+
'warning',
|
|
1506
|
+
'The verified device grant can load offline but must sync before grace expires.',
|
|
1507
|
+
));
|
|
1508
|
+
}
|
|
1509
|
+
return finalizeLoadPlan(plan);
|
|
1510
|
+
}
|
|
1438
1511
|
plan.state = 'needs_account';
|
|
1439
1512
|
plan.required_action = 'sign_in_or_activate';
|
|
1440
1513
|
plan.can_load_now = false;
|
|
@@ -1448,6 +1521,16 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1448
1521
|
}
|
|
1449
1522
|
|
|
1450
1523
|
if (plan.entitlement_profile === 'org') {
|
|
1524
|
+
if (isVerifiedExternalEntitlement(opts.entitlement)) {
|
|
1525
|
+
if (!externalEntitlementMatchesCurrentAsset(opts.entitlement)) {
|
|
1526
|
+
return rejectExternalEntitlementMismatch();
|
|
1527
|
+
}
|
|
1528
|
+
plan.state = opts.entitlement.status === 'offline_grace' ? 'offline_grace' : 'ready';
|
|
1529
|
+
plan.required_action = opts.entitlement.status === 'offline_grace' ? 'sync' : 'load';
|
|
1530
|
+
plan.can_load_now = true;
|
|
1531
|
+
plan.projection_policy = 'minimal';
|
|
1532
|
+
return finalizeLoadPlan(plan);
|
|
1533
|
+
}
|
|
1451
1534
|
plan.state = 'needs_org_auth';
|
|
1452
1535
|
plan.required_action = 'sign_in_or_activate';
|
|
1453
1536
|
plan.can_load_now = false;
|
|
@@ -1621,26 +1704,22 @@ function assertNoForbiddenTerms(obj) {
|
|
|
1621
1704
|
}
|
|
1622
1705
|
|
|
1623
1706
|
module.exports = {
|
|
1624
|
-
MIMETYPE
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
V1_REQUIRED_DIR_ENTRIES,
|
|
1628
|
-
isV1SourceDir,
|
|
1629
|
-
isV2SourceDir,
|
|
1707
|
+
MIMETYPE,
|
|
1708
|
+
REQUIRED_DIR_ENTRIES,
|
|
1709
|
+
isKdnaSourceDir,
|
|
1630
1710
|
detectContainerFormat,
|
|
1631
|
-
readV1Layout,
|
|
1711
|
+
readLayout: readV1Layout,
|
|
1632
1712
|
inspect,
|
|
1633
1713
|
validate,
|
|
1634
1714
|
planLoad,
|
|
1635
1715
|
loadAuthorized,
|
|
1636
|
-
buildChecksumsV1,
|
|
1716
|
+
buildChecksums: buildChecksumsV1,
|
|
1637
1717
|
pack,
|
|
1638
1718
|
unpack,
|
|
1639
|
-
loadV1,
|
|
1719
|
+
load: loadV1,
|
|
1720
|
+
loadAsset: loadV1,
|
|
1721
|
+
buildCapsule,
|
|
1640
1722
|
FORBIDDEN_OUTPUT_TERMS,
|
|
1641
|
-
// Semver utilities (Story 6 / Story 13) — exported so consumers
|
|
1642
|
-
// (e.g. kdna-cli) can use the canonical implementation instead of
|
|
1643
|
-
// duplicating it.
|
|
1644
1723
|
parseSemver,
|
|
1645
1724
|
compareSemver,
|
|
1646
1725
|
satisfies,
|
|
@@ -1742,8 +1821,11 @@ function loadAuthorized(inputPath, opts = {}) {
|
|
|
1742
1821
|
const mergedOpts = {
|
|
1743
1822
|
...opts,
|
|
1744
1823
|
resolvedDependencies: plan.resolved_dependencies,
|
|
1745
|
-
// Pass extends_chain from plan to loadV1Unsafe so inheritance is applied (Story 12)
|
|
1746
1824
|
extendsChain: plan.extends_chain || [],
|
|
1825
|
+
_validation: {
|
|
1826
|
+
schema_valid: plan.checks && plan.checks.overall_valid === true,
|
|
1827
|
+
signature_valid: plan.signature_valid === true,
|
|
1828
|
+
},
|
|
1747
1829
|
};
|
|
1748
1830
|
return loadV1Unsafe(inputPath, mergedOpts);
|
|
1749
1831
|
}
|
|
@@ -1793,9 +1875,9 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1793
1875
|
let payload;
|
|
1794
1876
|
try {
|
|
1795
1877
|
const rawPayload = v1.map['payload.kdnab'];
|
|
1796
|
-
payload =
|
|
1878
|
+
payload = parsePayloadEntry("payload.kdnab", rawPayload);
|
|
1797
1879
|
} catch (e) {
|
|
1798
|
-
throw new Error(`payload.kdnab is not valid
|
|
1880
|
+
throw new Error(`payload.kdnab is not valid KDNA payload: ${e.message}`, { cause: e });
|
|
1799
1881
|
}
|
|
1800
1882
|
|
|
1801
1883
|
if (payload.profile && payload.ciphertext && (m.payload?.encrypted || m.encryption?.encrypted_entries?.includes('payload.kdnab'))) {
|
|
@@ -1812,10 +1894,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1812
1894
|
const decryptedBuf = typeof decrypted === 'string'
|
|
1813
1895
|
? Buffer.from(decrypted, 'utf8')
|
|
1814
1896
|
: Buffer.from(decrypted);
|
|
1815
|
-
payload =
|
|
1897
|
+
payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
|
|
1816
1898
|
} catch (e) {
|
|
1817
1899
|
const err = new Error(`Decryption failed: ${e.message}`);
|
|
1818
|
-
err.code = 'KDNA_DECRYPT_FAILED';
|
|
1900
|
+
err.code = e.code || 'KDNA_DECRYPT_FAILED';
|
|
1819
1901
|
throw err;
|
|
1820
1902
|
}
|
|
1821
1903
|
} else if (opts.password) {
|
|
@@ -1827,8 +1909,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1827
1909
|
decryptProtectedEntryScrypt,
|
|
1828
1910
|
PASSWORD_PROTECTED_SCRYPT_PROFILE,
|
|
1829
1911
|
} = require('../crypto-profile.js');
|
|
1830
|
-
const encryptedEnvelope = v1.map['payload.kdnab']
|
|
1831
|
-
|
|
1912
|
+
const encryptedEnvelope = v1.map['payload.kdnab'];
|
|
1913
|
+
let envelope;
|
|
1914
|
+
try { envelope = cbor.decode(Buffer.isBuffer(encryptedEnvelope) ? encryptedEnvelope : Buffer.from(encryptedEnvelope)); }
|
|
1915
|
+
catch { throw new Error('Cannot decode encrypted envelope: payload.kdnab must be CBOR-encoded (kdna_version 1.0)'); }
|
|
1832
1916
|
const decryptFn = (envelope.profile === PASSWORD_PROTECTED_SCRYPT_PROFILE)
|
|
1833
1917
|
? decryptProtectedEntryScrypt
|
|
1834
1918
|
: decryptProtectedEntry;
|
|
@@ -1837,15 +1921,20 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1837
1921
|
manifest: m,
|
|
1838
1922
|
password: opts.password,
|
|
1839
1923
|
});
|
|
1840
|
-
payload =
|
|
1924
|
+
payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
|
|
1841
1925
|
} catch (e) {
|
|
1842
1926
|
const err = new Error(`Decryption failed with provided password: ${e.message}`);
|
|
1843
1927
|
err.code = 'KDNA_DECRYPT_FAILED';
|
|
1844
1928
|
throw err;
|
|
1845
1929
|
}
|
|
1846
1930
|
} else {
|
|
1847
|
-
const
|
|
1848
|
-
err
|
|
1931
|
+
const isExternal = payload.profile === EXTERNAL_ENVELOPE_PROFILE;
|
|
1932
|
+
const err = new Error(
|
|
1933
|
+
isExternal
|
|
1934
|
+
? 'payload requires a verified account/device external key grant'
|
|
1935
|
+
: 'payload is encrypted and cannot be decrypted without an authorized credential',
|
|
1936
|
+
);
|
|
1937
|
+
err.code = isExternal ? 'KDNA_AUTH_ACCOUNT_REQUIRED' : 'KDNA_AUTH_PASSWORD_REQUIRED';
|
|
1849
1938
|
throw err;
|
|
1850
1939
|
}
|
|
1851
1940
|
}
|
|
@@ -1876,7 +1965,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1876
1965
|
const hasJudgment = (core.axioms && core.axioms.length > 0)
|
|
1877
1966
|
|| (core.boundaries && core.boundaries.length > 0)
|
|
1878
1967
|
|| (payload.patterns && payload.patterns.length > 0)
|
|
1879
|
-
|| (payload.reasoning && ((payload.reasoning.
|
|
1968
|
+
|| (payload.reasoning && ((payload.reasoning.self_check && payload.reasoning.self_check.length > 0) || (payload.reasoning.failure_modes && payload.reasoning.failure_modes.length > 0)));
|
|
1880
1969
|
profiles.push('index');
|
|
1881
1970
|
if (hasJudgment) profiles.push('compact');
|
|
1882
1971
|
if (payload.scenarios && payload.scenarios.length > 0) profiles.push('scenario');
|
|
@@ -1911,7 +2000,11 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1911
2000
|
highest_question: core.highest_question || null,
|
|
1912
2001
|
axioms: (core.axioms || []).map(normalizeCompactAxiom).filter(Boolean),
|
|
1913
2002
|
boundaries: normalizeList(core.boundaries),
|
|
1914
|
-
|
|
2003
|
+
// The canonical payload field is singular (`reasoning.self_check`).
|
|
2004
|
+
// Runtime Capsule uses plural (`context.self_checks`) as its projection
|
|
2005
|
+
// field. Reading the plural name from the payload silently discarded
|
|
2006
|
+
// every Studio-authored self-check.
|
|
2007
|
+
self_checks: normalizeList(payload.reasoning && payload.reasoning.self_check),
|
|
1915
2008
|
failure_modes: normalizeList(payload.reasoning && payload.reasoning.failure_modes),
|
|
1916
2009
|
patterns: normalizeList(payload.patterns).slice(0, 3),
|
|
1917
2010
|
};
|
|
@@ -1938,8 +2031,9 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1938
2031
|
resolvedDependencies: [],
|
|
1939
2032
|
extendsChain: [],
|
|
1940
2033
|
});
|
|
1941
|
-
|
|
1942
|
-
|
|
2034
|
+
const baseContent = baseLoaded && (baseLoaded.content || baseLoaded.context);
|
|
2035
|
+
if (baseContent) {
|
|
2036
|
+
const bc = baseContent;
|
|
1943
2037
|
const cc = result.content;
|
|
1944
2038
|
// Merge axioms: child overrides by id; parent axioms not in child are added
|
|
1945
2039
|
if (bc.axioms || cc.axioms) {
|
|
@@ -2048,9 +2142,65 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2048
2142
|
return { status: result.status, profile: result.profile, text: textOut };
|
|
2049
2143
|
}
|
|
2050
2144
|
|
|
2145
|
+
if (as === 'prompt') {
|
|
2146
|
+
return result;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
return wrapAsCapsule(result, v1, profile, opts);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function wrapAsCapsule(result, v1, profile, opts) {
|
|
2153
|
+
if (opts.as === 'json') {
|
|
2154
|
+
return buildCapsule(result, v1, profile, opts);
|
|
2155
|
+
}
|
|
2051
2156
|
return result;
|
|
2052
2157
|
}
|
|
2053
2158
|
|
|
2159
|
+
function buildCapsule(loadResult, v1, profile, opts = {}) {
|
|
2160
|
+
const m = v1.manifest;
|
|
2161
|
+
const val = opts._validation || {};
|
|
2162
|
+
const assetDigest = v1.assetDigest || (v1.map && v1.map['checksums.json']
|
|
2163
|
+
? (() => {
|
|
2164
|
+
try {
|
|
2165
|
+
const checks = JSON.parse(v1.map['checksums.json'].toString('utf8'));
|
|
2166
|
+
return checks.asset_digest || null;
|
|
2167
|
+
} catch { return null; }
|
|
2168
|
+
})()
|
|
2169
|
+
: null);
|
|
2170
|
+
|
|
2171
|
+
const pubkey = (m.creator && m.creator.pubkey) || (m.author && m.author.pubkey) || null;
|
|
2172
|
+
const sigVerified = val.signature_valid === true;
|
|
2173
|
+
const capsule = {
|
|
2174
|
+
type: 'kdna.context.capsule',
|
|
2175
|
+
version: '1.0',
|
|
2176
|
+
domain: m.name || m.asset_id || null,
|
|
2177
|
+
judgment_version: m.judgment_version || null,
|
|
2178
|
+
asset_digest: assetDigest,
|
|
2179
|
+
signature: pubkey
|
|
2180
|
+
? { state: sigVerified ? 'verified' : 'not_checked', issuer: pubkey }
|
|
2181
|
+
: { state: 'absent' },
|
|
2182
|
+
access: m.access || 'public',
|
|
2183
|
+
risk_level: m.risk_level || null,
|
|
2184
|
+
profile: loadResult.profile,
|
|
2185
|
+
context: loadResult.content || {},
|
|
2186
|
+
trace: {
|
|
2187
|
+
payload_encoding: (m.payload && m.payload.encoding) || 'cbor',
|
|
2188
|
+
loaded_by: 'kdna-core',
|
|
2189
|
+
loaded_at: new Date().toISOString(),
|
|
2190
|
+
schema_valid: val.schema_valid === true,
|
|
2191
|
+
signature_state: sigVerified ? 'verified' : (pubkey ? 'not_checked' : 'absent'),
|
|
2192
|
+
profile: profile,
|
|
2193
|
+
},
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
if (loadResult.extends_chain) capsule.extends_chain = loadResult.extends_chain;
|
|
2197
|
+
if (loadResult.inheritance_applied !== undefined) capsule.inheritance_applied = loadResult.inheritance_applied;
|
|
2198
|
+
if (loadResult.resolved_dependencies) capsule.resolved_dependencies = loadResult.resolved_dependencies;
|
|
2199
|
+
if (loadResult.rag_isolation_policy) capsule.rag_isolation_policy = loadResult.rag_isolation_policy;
|
|
2200
|
+
|
|
2201
|
+
return capsule;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2054
2204
|
// ─── Semver & Dependency Resolution (Story 6) ─────────────────────────
|
|
2055
2205
|
|
|
2056
2206
|
function parseSemver(v) {
|
package/src/v1/index.mjs
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import v1 from './index.js';
|
|
2
2
|
|
|
3
3
|
export const MIMETYPE = v1.MIMETYPE;
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
export const V1_REQUIRED_DIR_ENTRIES = v1.V1_REQUIRED_DIR_ENTRIES;
|
|
7
|
-
export const isV1SourceDir = v1.isV1SourceDir;
|
|
8
|
-
export const isV2SourceDir = v1.isV2SourceDir;
|
|
4
|
+
export const REQUIRED_DIR_ENTRIES = v1.REQUIRED_DIR_ENTRIES;
|
|
5
|
+
export const isKdnaSourceDir = v1.isKdnaSourceDir;
|
|
9
6
|
export const detectContainerFormat = v1.detectContainerFormat;
|
|
10
|
-
export const
|
|
7
|
+
export const readLayout = v1.readLayout;
|
|
11
8
|
export const inspect = v1.inspect;
|
|
12
9
|
export const validate = v1.validate;
|
|
13
10
|
export const planLoad = v1.planLoad;
|
|
14
11
|
export const loadAuthorized = v1.loadAuthorized;
|
|
15
|
-
export const
|
|
12
|
+
export const buildChecksums = v1.buildChecksums;
|
|
16
13
|
export const pack = v1.pack;
|
|
17
14
|
export const unpack = v1.unpack;
|
|
18
|
-
export const
|
|
15
|
+
export const load = v1.load;
|
|
16
|
+
export const loadAsset = v1.loadAsset;
|
|
17
|
+
export const buildCapsule = v1.buildCapsule;
|
|
19
18
|
export const FORBIDDEN_OUTPUT_TERMS = v1.FORBIDDEN_OUTPUT_TERMS;
|
|
20
19
|
|
|
21
20
|
export default v1;
|