@aikdna/kdna-core 0.12.3 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,27 +52,35 @@ const compact = loadAuthorized('./asset.kdna', {
52
52
  ## Supported Runtime Flow
53
53
 
54
54
  ```text
55
- v1 source directory
56
- → buildChecksumsV1
57
- → pack
55
+ .kdna file
58
56
  → validate
59
57
  → planLoad
60
58
  → loadAuthorized
61
59
  → agent/runtime context
62
60
  ```
63
61
 
64
- ## v1 Source Directory
62
+ For authoring and test fixtures, `pack()` can turn a local working folder into
63
+ a `.kdna` file before validation:
64
+
65
+ ```text
66
+ local working folder
67
+ → pack
68
+ → .kdna file
69
+ → validate
70
+ → planLoad
71
+ → loadAuthorized
72
+ ```
73
+
74
+ ## v1 Container
65
75
 
66
- A v1 source directory contains:
76
+ A v1 `.kdna` container contains:
67
77
 
68
78
  - `mimetype`
69
79
  - `kdna.json`
70
80
  - `payload.kdnab`
71
81
  - `checksums.json`
72
82
 
73
- ## v1 Container
74
-
75
- A v1 `.kdna` container is a zip package of the same files. `validate()` checks:
83
+ `validate()` checks:
76
84
 
77
85
  - format
78
86
  - manifest schema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-core",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -6,6 +6,16 @@
6
6
  "type": "object",
7
7
  "required": ["algorithm"],
8
8
  "additionalProperties": true,
9
+ "$defs": {
10
+ "sha256Digest": {
11
+ "type": "string",
12
+ "pattern": "^sha256:[a-f0-9]{64}$"
13
+ },
14
+ "sha256Value": {
15
+ "type": "string",
16
+ "pattern": "^[a-f0-9]{64}$"
17
+ }
18
+ },
9
19
  "properties": {
10
20
  "algorithm": {
11
21
  "type": "string",
@@ -13,19 +23,16 @@
13
23
  "enum": ["sha256", "sha512", "blake2b-256"]
14
24
  },
15
25
  "manifest_digest": {
16
- "type": "string",
17
26
  "description": "Digest of the kdna.json entry.",
18
- "pattern": "^[a-z0-9-]+:.+$"
27
+ "$ref": "#/$defs/sha256Digest"
19
28
  },
20
29
  "payload_digest": {
21
- "type": "string",
22
30
  "description": "Digest of the payload.kdnab entry.",
23
- "pattern": "^[a-z0-9-]+:.+$"
31
+ "$ref": "#/$defs/sha256Digest"
24
32
  },
25
33
  "asset_digest": {
26
- "type": "string",
27
34
  "description": "Digest of the whole container (per-entry digests combined deterministically).",
28
- "pattern": "^[a-z0-9-]+:.+$"
35
+ "$ref": "#/$defs/sha256Digest"
29
36
  },
30
37
  "entries": {
31
38
  "type": "object",
@@ -34,8 +41,8 @@
34
41
  "type": "object",
35
42
  "required": ["algorithm", "value"],
36
43
  "properties": {
37
- "algorithm": { "type": "string" },
38
- "value": { "type": "string" }
44
+ "algorithm": { "const": "sha256" },
45
+ "value": { "$ref": "#/$defs/sha256Value" }
39
46
  }
40
47
  }
41
48
  }
@@ -14,6 +14,7 @@
14
14
  "required_action",
15
15
  "can_load_now",
16
16
  "projection_policy",
17
+ "input_fingerprint",
17
18
  "checks",
18
19
  "issues",
19
20
  "source"
@@ -73,6 +74,10 @@
73
74
  "type": "string",
74
75
  "enum": ["minimal", "remote", "none"]
75
76
  },
77
+ "input_fingerprint": {
78
+ "type": ["string", "null"],
79
+ "pattern": "^sha256:[a-f0-9]{64}$"
80
+ },
76
81
  "checks": {
77
82
  "type": "object",
78
83
  "required": [
package/src/types.d.ts CHANGED
@@ -549,6 +549,7 @@ export interface KDNAV1LoadPlan {
549
549
  | string;
550
550
  can_load_now: boolean;
551
551
  projection_policy: 'minimal' | 'remote' | 'none' | string;
552
+ input_fingerprint: string | null;
552
553
  checks: Record<string, boolean>;
553
554
  issues: KDNAV1LoadPlanIssue[];
554
555
  source: {
package/src/v1/index.js CHANGED
@@ -747,6 +747,7 @@ function verifyDigests(checksums, map, problems, result) {
747
747
  manifest_digest: 'kdna.json',
748
748
  payload_digest: 'payload.kdnab',
749
749
  };
750
+ const entryDigests = {};
750
751
  let stillValid = true;
751
752
  for (const [digestKey, entryName] of Object.entries(entryMap)) {
752
753
  const declared = checksums[digestKey];
@@ -758,12 +759,33 @@ function verifyDigests(checksums, map, problems, result) {
758
759
  }
759
760
  const entryBytes = map[entryName];
760
761
  const actual = crypto.createHash('sha256').update(entryBytes).digest('hex');
762
+ entryDigests[entryName] = actual;
761
763
  const expected = declared.replace(/^sha256:/, '');
762
764
  if (actual !== expected) {
763
765
  problems.push(`checksums: ${digestKey} mismatch (declared ${expected.slice(0, 8)}..., actual ${actual.slice(0, 8)}...)`);
764
766
  stillValid = false;
765
767
  }
766
768
  }
769
+ if (checksums.asset_digest) {
770
+ const expectedAssetDigest = checksums.asset_digest.replace(/^sha256:/, '');
771
+ const missingAssetInputs = ['kdna.json', 'payload.kdnab'].filter((entryName) => !entryDigests[entryName]);
772
+ if (missingAssetInputs.length) {
773
+ problems.push(`checksums: asset_digest cannot be verified without ${missingAssetInputs.join(', ')}`);
774
+ stillValid = false;
775
+ } else {
776
+ const combined = Object.keys(entryDigests)
777
+ .sort()
778
+ .map((name) => `${name}:${entryDigests[name]}`)
779
+ .join('\n');
780
+ const actualAssetDigest = crypto.createHash('sha256').update(combined).digest('hex');
781
+ if (actualAssetDigest !== expectedAssetDigest) {
782
+ problems.push(
783
+ `checksums: asset_digest mismatch (declared ${expectedAssetDigest.slice(0, 8)}..., actual ${actualAssetDigest.slice(0, 8)}...)`,
784
+ );
785
+ stillValid = false;
786
+ }
787
+ }
788
+ }
767
789
  if (!stillValid) result.checksums_valid = false;
768
790
  return stillValid;
769
791
  }
@@ -986,6 +1008,25 @@ function finalizeLoadPlan(plan) {
986
1008
  return plan;
987
1009
  }
988
1010
 
1011
+ function inputFingerprint(inputPath, v1) {
1012
+ const absPath = path.resolve(inputPath);
1013
+ if (v1.kind === 'file') {
1014
+ return `sha256:${crypto.createHash('sha256').update(fs.readFileSync(absPath)).digest('hex')}`;
1015
+ }
1016
+
1017
+ const hash = crypto.createHash('sha256');
1018
+ for (const name of Object.keys(v1.map).sort()) {
1019
+ const bytes = v1.map[name];
1020
+ hash.update(name);
1021
+ hash.update('\0');
1022
+ hash.update(String(bytes.length));
1023
+ hash.update('\0');
1024
+ hash.update(bytes);
1025
+ hash.update('\0');
1026
+ }
1027
+ return `sha256:${hash.digest('hex')}`;
1028
+ }
1029
+
989
1030
  function baseLoadPlan(inputPath, v1, validation) {
990
1031
  const manifest = v1.manifest;
991
1032
  const accessInfo = normalizeAccess(manifest.access);
@@ -1008,6 +1049,7 @@ function baseLoadPlan(inputPath, v1, validation) {
1008
1049
  required_action: 'block',
1009
1050
  can_load_now: false,
1010
1051
  projection_policy: 'none',
1052
+ input_fingerprint: inputFingerprint(inputPath, v1),
1011
1053
  checks: {
1012
1054
  format_valid: validation.format_valid,
1013
1055
  schema_valid: validation.schema_valid,
@@ -1060,6 +1102,7 @@ function planLoad(inputPath, opts = {}) {
1060
1102
  required_action: 'block',
1061
1103
  can_load_now: false,
1062
1104
  projection_policy: 'none',
1105
+ input_fingerprint: null,
1063
1106
  checks: {
1064
1107
  format_valid: false,
1065
1108
  schema_valid: false,