@aikdna/kdna-core 0.15.11 → 0.15.12
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 +33 -0
- package/README.md +24 -21
- package/package.json +2 -7
- 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/index.mjs +22 -0
- package/src/lint-pure.js +8 -13
- package/src/types.d.ts +15 -19
- package/src/v1/index.js +138 -66
- package/src/v1/index.mjs +7 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.15.12 (2026-07-12)
|
|
4
|
+
|
|
5
|
+
**Single-format refactor + CBOR wire contract + Runtime Capsule.**
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- `MIMETYPE_V1`, `MIMETYPE_V2`, `MIMETYPE_LEGACY`, `isV1SourceDir`, `isV2SourceDir`
|
|
10
|
+
removed from public API. Use `MIMETYPE`, `isKdnaSourceDir`, `detectContainerFormat`.
|
|
11
|
+
- `detectContainerFormat` returns only `"kdna"` or `null`.
|
|
12
|
+
- `kdna_version: "2.0"` is no longer accepted. Only `"1.0"`.
|
|
13
|
+
- `payload.encoding` accepts only `"cbor"`. JSON payload bytes are rejected;
|
|
14
|
+
there is no production JSON fallback.
|
|
15
|
+
- Version-qualified public APIs (`V1_REQUIRED_DIR_ENTRIES`, `readV1Layout`,
|
|
16
|
+
`buildChecksumsV1`, `loadV1`, and the `./v1` export) are removed. Use the
|
|
17
|
+
unversioned APIs below.
|
|
18
|
+
- `kdna load --as=json` now returns Runtime Capsule format
|
|
19
|
+
(`type: "kdna.context.capsule"`) instead of raw `{status, content}`.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `REQUIRED_DIR_ENTRIES`, `readLayout`, `buildChecksums`, `load`, and
|
|
24
|
+
`loadAsset` are the sole public container/loading APIs.
|
|
25
|
+
- `buildCapsule(loadResult, v1, profile, opts)` — wraps load output as
|
|
26
|
+
`kdna.context.capsule` per `specs/runtime-capsule.md`.
|
|
27
|
+
- `parsePayloadEntry` — strict CBOR decoding for `payload.kdnab`.
|
|
28
|
+
|
|
29
|
+
### Fixes
|
|
30
|
+
|
|
31
|
+
- Producer/consumer encoding alignment: `dev-pack.js`, `studio.js` write CBOR;
|
|
32
|
+
validation and loading decode CBOR, including encrypted envelopes.
|
|
33
|
+
- `loadV1Unsafe` extends/deps processing occurs before Capsule wrapping (Story 12 fix).
|
|
34
|
+
- All test fixtures converted to CBOR; golden files regenerated.
|
|
35
|
+
|
|
3
36
|
## v0.15.11 (2026-07-03)
|
|
4
37
|
|
|
5
38
|
**Fix**: repair Core v1 container dispatcher module resolution in the
|
package/README.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# @aikdna/kdna-core
|
|
2
2
|
|
|
3
|
-
Core library for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Studio export, skills, MCP integrations, and downstream agent runtimes.
|
|
3
|
+
Core library for `.kdna` judgment assets. It implements the single KDNA Asset
|
|
4
|
+
Container, strict-CBOR payload decoding, validation, LoadPlan authorization,
|
|
5
|
+
and Runtime Capsule projection used by the CLI, Studio, skills, MCP
|
|
6
|
+
integrations, and downstream Agent runtimes.
|
|
8
7
|
|
|
9
8
|
## Installation
|
|
10
9
|
|
|
@@ -18,9 +17,10 @@ If you need full JSON Schema validation through Ajv, also install:
|
|
|
18
17
|
npm install ajv ajv-formats
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
##
|
|
20
|
+
## Public API
|
|
22
21
|
|
|
23
|
-
Use the
|
|
22
|
+
Use the package root. Version-qualified entrypoints and API names are not part
|
|
23
|
+
of the public contract.
|
|
24
24
|
|
|
25
25
|
```js
|
|
26
26
|
const {
|
|
@@ -30,8 +30,8 @@ const {
|
|
|
30
30
|
loadAuthorized,
|
|
31
31
|
pack,
|
|
32
32
|
unpack,
|
|
33
|
-
|
|
34
|
-
} = require('@aikdna/kdna-core
|
|
33
|
+
buildChecksums
|
|
34
|
+
} = require('@aikdna/kdna-core');
|
|
35
35
|
|
|
36
36
|
const validation = validate('./asset.kdna');
|
|
37
37
|
if (!validation.overall_valid) {
|
|
@@ -43,10 +43,14 @@ if (plan.can_load_now !== true) {
|
|
|
43
43
|
throw new Error(`Asset is not loadable yet: ${plan.required_action}`);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const
|
|
46
|
+
const capsule = loadAuthorized('./asset.kdna', {
|
|
47
47
|
profile: 'compact',
|
|
48
|
-
as: '
|
|
48
|
+
as: 'json'
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
if (capsule.type !== 'kdna.context.capsule') {
|
|
52
|
+
throw new Error('Expected a Runtime Capsule');
|
|
53
|
+
}
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
## Supported Runtime Flow
|
|
@@ -56,7 +60,8 @@ const compact = loadAuthorized('./asset.kdna', {
|
|
|
56
60
|
→ validate
|
|
57
61
|
→ planLoad
|
|
58
62
|
→ loadAuthorized
|
|
59
|
-
→
|
|
63
|
+
→ Runtime Capsule
|
|
64
|
+
→ Agent/runtime context
|
|
60
65
|
```
|
|
61
66
|
|
|
62
67
|
For authoring and test fixtures, `pack()` can turn a local working folder into
|
|
@@ -71,9 +76,9 @@ local working folder
|
|
|
71
76
|
→ loadAuthorized
|
|
72
77
|
```
|
|
73
78
|
|
|
74
|
-
##
|
|
79
|
+
## KDNA Asset Container
|
|
75
80
|
|
|
76
|
-
A
|
|
81
|
+
A `.kdna` asset contains:
|
|
77
82
|
|
|
78
83
|
- `mimetype`
|
|
79
84
|
- `kdna.json`
|
|
@@ -88,6 +93,10 @@ A v1 `.kdna` container contains:
|
|
|
88
93
|
- checksum consistency
|
|
89
94
|
- load-profile contract
|
|
90
95
|
|
|
96
|
+
`payload.kdnab` is CBOR. A password-protected or licensed asset stores a CBOR
|
|
97
|
+
encryption envelope in the same entry; authorized plaintext is decrypted in
|
|
98
|
+
memory and projected into the Runtime Capsule.
|
|
99
|
+
|
|
91
100
|
## Load Profiles
|
|
92
101
|
|
|
93
102
|
`loadAuthorized()` supports:
|
|
@@ -104,15 +113,9 @@ Output formats:
|
|
|
104
113
|
|
|
105
114
|
## Boundary
|
|
106
115
|
|
|
107
|
-
KDNA Core
|
|
116
|
+
KDNA Core is content-neutral. It validates file structure and loadability;
|
|
108
117
|
it does not rank judgment quality or endorse specific assets.
|
|
109
118
|
|
|
110
|
-
## Legacy API
|
|
111
|
-
|
|
112
|
-
The package root still exports compatibility APIs for older KDNA paths. New
|
|
113
|
-
tooling should prefer `@aikdna/kdna-core/v1` and the `planLoad` /
|
|
114
|
-
`loadAuthorized` path.
|
|
115
|
-
|
|
116
119
|
## License
|
|
117
120
|
|
|
118
121
|
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.12",
|
|
4
4
|
"description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -15,12 +15,7 @@
|
|
|
15
15
|
"types": "./src/types.d.ts"
|
|
16
16
|
},
|
|
17
17
|
"./package.json": "./package.json",
|
|
18
|
-
"./schema/*": "./schema/*"
|
|
19
|
-
"./v1": {
|
|
20
|
-
"import": "./src/v1/index.mjs",
|
|
21
|
-
"require": "./src/v1/index.js",
|
|
22
|
-
"types": "./src/types.d.ts"
|
|
23
|
-
}
|
|
18
|
+
"./schema/*": "./schema/*"
|
|
24
19
|
},
|
|
25
20
|
"types": "src/types.d.ts",
|
|
26
21
|
"scripts": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"properties": {
|
|
23
23
|
"kdna_version": {
|
|
24
24
|
"type": "string",
|
|
25
|
-
"description": "KDNA
|
|
26
|
-
"enum": ["1.0"
|
|
25
|
+
"description": "KDNA Asset Container wire discriminator.",
|
|
26
|
+
"enum": ["1.0"]
|
|
27
27
|
},
|
|
28
28
|
"asset_id": {
|
|
29
29
|
"type": "string",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"additionalProperties": true,
|
|
103
103
|
"properties": {
|
|
104
104
|
"path": { "type": "string", "const": "payload.kdnab" },
|
|
105
|
-
"encoding": { "type": "string", "enum": ["
|
|
105
|
+
"encoding": { "type": "string", "enum": ["cbor"] },
|
|
106
106
|
"encrypted": { "type": "boolean" }
|
|
107
107
|
}
|
|
108
108
|
},
|
package/src/asset-reader.js
CHANGED
|
@@ -8,7 +8,7 @@ const zlib = require('zlib');
|
|
|
8
8
|
const cbor = require('cbor-x');
|
|
9
9
|
const { loadDomainFromFiles, formatContext } = require('./loader');
|
|
10
10
|
|
|
11
|
-
const KDNA_MEDIA_TYPE = 'application/vnd.
|
|
11
|
+
const KDNA_MEDIA_TYPE = 'application/vnd.kdna.asset';
|
|
12
12
|
|
|
13
13
|
// Standard KDNA domain data entries — kept in lockstep with loader.FILE_MAP.
|
|
14
14
|
// Asset reader pre-loads these so callers don't have to enumerate entry names.
|
|
@@ -363,19 +363,18 @@ function verifyHumanLockSignatures(coreData, manifest, errors, warnings) {
|
|
|
363
363
|
|
|
364
364
|
function validateManifestIdentity(manifest, errors, _warnings) {
|
|
365
365
|
if (manifest.kdna_spec) {
|
|
366
|
-
errors.push('kdna.json: kdna_spec is not allowed. Use
|
|
366
|
+
errors.push('kdna.json: kdna_spec is not allowed. Use kdna_version.');
|
|
367
367
|
}
|
|
368
368
|
if (manifest.format && manifest.format !== 'kdna') {
|
|
369
369
|
errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
|
|
370
370
|
}
|
|
371
|
-
if (manifest.
|
|
371
|
+
if (!manifest.kdna_version) {
|
|
372
|
+
errors.push('kdna.json: missing required field "kdna_version"');
|
|
373
|
+
} else if (manifest.kdna_version !== '1.0') {
|
|
372
374
|
errors.push(
|
|
373
|
-
`kdna.json.
|
|
375
|
+
`kdna.json.kdna_version: invalid value "${manifest.kdna_version}". Expected "1.0".`,
|
|
374
376
|
);
|
|
375
377
|
}
|
|
376
|
-
if (!manifest.spec_version) errors.push('kdna.json: missing required field "spec_version"');
|
|
377
|
-
if (!manifest.format) errors.push('kdna.json: missing required field "format"');
|
|
378
|
-
if (!manifest.format_version) errors.push('kdna.json: missing required field "format_version"');
|
|
379
378
|
if (manifest.language) {
|
|
380
379
|
errors.push('kdna.json: language is not allowed. Use default_language and languages.');
|
|
381
380
|
}
|
|
@@ -624,10 +623,7 @@ function createKdnaAssetReader() {
|
|
|
624
623
|
|
|
625
624
|
if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
|
|
626
625
|
verifyMediaType(asset, errors);
|
|
627
|
-
if (!asset.entries.has('
|
|
628
|
-
if (!asset.entries.has('KDNA_Patterns.json')) {
|
|
629
|
-
errors.push('required entry missing: KDNA_Patterns.json');
|
|
630
|
-
}
|
|
626
|
+
if (!asset.entries.has('payload.kdnab')) errors.push('required entry missing: payload.kdnab');
|
|
631
627
|
|
|
632
628
|
const content_digest = buildContentDigest(asset);
|
|
633
629
|
const asset_digest = asset.asset_digest;
|
|
@@ -4,45 +4,25 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const crypto = require('node:crypto');
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const MIMETYPE_V1 = 'application/vnd.kdna.asset';
|
|
10
|
-
const MIMETYPE_V2 = 'application/vnd.aikdna.kdna+zip';
|
|
7
|
+
const MIMETYPE = 'application/vnd.kdna.asset';
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
|
-
* CanonicalAssetModel — the single internal representation of
|
|
14
|
-
* regardless of
|
|
15
|
-
* or Canonical Distribution Container (Container v2 / SPEC 2.0).
|
|
16
|
-
*
|
|
17
|
-
* All downstream code (planLoad, loadAuthorized, decrypt, loadV1Unsafe)
|
|
18
|
-
* operates on this model. No consumer should check container format directly.
|
|
10
|
+
* CanonicalAssetModel — the single internal representation of a KDNA asset,
|
|
11
|
+
* regardless of source (directory or .kdna container).
|
|
19
12
|
*
|
|
20
13
|
* @typedef {object} CanonicalAssetModel
|
|
21
|
-
* @property {'dir'|'file'} sourceKind
|
|
22
|
-
* @property {string} sourcePath
|
|
23
|
-
* @property {string} format — '
|
|
24
|
-
* @property {string} mimetype
|
|
14
|
+
* @property {'dir'|'file'} sourceKind
|
|
15
|
+
* @property {string} sourcePath
|
|
16
|
+
* @property {string} format — 'dir' | 'kdna'
|
|
17
|
+
* @property {string} mimetype
|
|
25
18
|
* @property {object} manifest — parsed kdna.json
|
|
26
|
-
* @property {Buffer} payloadRaw — raw payload.kdnab bytes
|
|
27
|
-
* @property {object|null} checksums
|
|
28
|
-
* @property {Buffer|null} assetDigest
|
|
29
|
-
* @property {Buffer|null} containerBuffer
|
|
30
|
-
* @property {Map<string, Buffer>|null} entries
|
|
19
|
+
* @property {Buffer} payloadRaw — raw payload.kdnab bytes
|
|
20
|
+
* @property {object|null} checksums
|
|
21
|
+
* @property {Buffer|null} assetDigest
|
|
22
|
+
* @property {Buffer|null} containerBuffer
|
|
23
|
+
* @property {Map<string, Buffer>|null} entries
|
|
31
24
|
*/
|
|
32
25
|
|
|
33
|
-
/**
|
|
34
|
-
* Read any KDNA asset into a CanonicalAssetModel.
|
|
35
|
-
*
|
|
36
|
-
* Handles:
|
|
37
|
-
* - Source directories (mimetype + kdna.json + payload.kdnab on disk)
|
|
38
|
-
* - Container v1 (.kdna ZIP with application/vnd.kdna.asset mimetype)
|
|
39
|
-
* - Canonical containers (.kdna ZIP with application/vnd.aikdna.kdna+zip mimetype)
|
|
40
|
-
*
|
|
41
|
-
* @param {string} inputPath - absolute path to asset file or directory
|
|
42
|
-
* @param {object} [opts]
|
|
43
|
-
* @param {object} [opts.limits] - container size/entry limits
|
|
44
|
-
* @returns {CanonicalAssetModel}
|
|
45
|
-
*/
|
|
46
26
|
function readAsset(inputPath, opts = {}) {
|
|
47
27
|
const absPath = path.resolve(inputPath);
|
|
48
28
|
const stat = fs.statSync(absPath);
|
|
@@ -59,11 +39,8 @@ function readAsset(inputPath, opts = {}) {
|
|
|
59
39
|
}
|
|
60
40
|
|
|
61
41
|
const format = detectFormat(absPath);
|
|
62
|
-
if (format === '
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
if (format === 'v2-container') {
|
|
66
|
-
return readV2Container(absPath, opts);
|
|
42
|
+
if (format === 'kdna') {
|
|
43
|
+
return readKdnaContainer(absPath, opts);
|
|
67
44
|
}
|
|
68
45
|
|
|
69
46
|
throw Object.assign(
|
|
@@ -72,10 +49,6 @@ function readAsset(inputPath, opts = {}) {
|
|
|
72
49
|
);
|
|
73
50
|
}
|
|
74
51
|
|
|
75
|
-
/**
|
|
76
|
-
* Detect the container format of a .kdna file.
|
|
77
|
-
* Returns 'v1-container', 'v2-container', or throws.
|
|
78
|
-
*/
|
|
79
52
|
function detectFormat(absPath) {
|
|
80
53
|
const buf = fs.readFileSync(absPath);
|
|
81
54
|
if (buf.length < 4) {
|
|
@@ -85,7 +58,6 @@ function detectFormat(absPath) {
|
|
|
85
58
|
);
|
|
86
59
|
}
|
|
87
60
|
|
|
88
|
-
// Must be a ZIP (PK signature)
|
|
89
61
|
const sig = buf.readUInt32LE(0);
|
|
90
62
|
if (sig !== 0x04034b50) {
|
|
91
63
|
throw Object.assign(
|
|
@@ -94,7 +66,6 @@ function detectFormat(absPath) {
|
|
|
94
66
|
);
|
|
95
67
|
}
|
|
96
68
|
|
|
97
|
-
// Read the mimetype from the first entry (central directory first entry)
|
|
98
69
|
const eocdOffset = findEocd(buf);
|
|
99
70
|
let cdOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
100
71
|
const cdEntryCount = buf.readUInt16LE(eocdOffset + 10);
|
|
@@ -126,8 +97,7 @@ function detectFormat(absPath) {
|
|
|
126
97
|
const compressedSize = buf.readUInt32LE(entryOffset + 20);
|
|
127
98
|
const mime = buf.toString('utf8', dataOffset, dataOffset + compressedSize).trim();
|
|
128
99
|
|
|
129
|
-
if (mime ===
|
|
130
|
-
if (mime === MIMETYPE_V2) return 'v2-container';
|
|
100
|
+
if (mime === MIMETYPE) return 'kdna';
|
|
131
101
|
|
|
132
102
|
throw Object.assign(
|
|
133
103
|
new Error(`Unknown KDNA mimetype: ${mime}`),
|
|
@@ -154,9 +124,6 @@ function findEocd(buf) {
|
|
|
154
124
|
);
|
|
155
125
|
}
|
|
156
126
|
|
|
157
|
-
/**
|
|
158
|
-
* Read a source directory into CanonicalAssetModel.
|
|
159
|
-
*/
|
|
160
127
|
function readSourceDirectory(absPath, _opts) {
|
|
161
128
|
const mimetypePath = path.join(absPath, 'mimetype');
|
|
162
129
|
const manifestPath = path.join(absPath, 'kdna.json');
|
|
@@ -171,7 +138,7 @@ function readSourceDirectory(absPath, _opts) {
|
|
|
171
138
|
}
|
|
172
139
|
|
|
173
140
|
const mimetype = fs.readFileSync(mimetypePath, 'utf8').trim();
|
|
174
|
-
if (
|
|
141
|
+
if (mimetype !== MIMETYPE) {
|
|
175
142
|
throw Object.assign(
|
|
176
143
|
new Error(`Unknown mimetype in source directory: ${mimetype}`),
|
|
177
144
|
{ code: 'KDNA_FORMAT_UNKNOWN' },
|
|
@@ -204,7 +171,7 @@ function readSourceDirectory(absPath, _opts) {
|
|
|
204
171
|
return {
|
|
205
172
|
sourceKind: 'dir',
|
|
206
173
|
sourcePath: absPath,
|
|
207
|
-
format: '
|
|
174
|
+
format: 'dir',
|
|
208
175
|
mimetype,
|
|
209
176
|
manifest,
|
|
210
177
|
payloadRaw,
|
|
@@ -215,21 +182,16 @@ function readSourceDirectory(absPath, _opts) {
|
|
|
215
182
|
};
|
|
216
183
|
}
|
|
217
184
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
*/
|
|
221
|
-
function readV1Container(absPath, _opts) {
|
|
222
|
-
// Delegate to the existing v1 layout reader for validation
|
|
223
|
-
const v1Layout = require('./v1/index.js').readV1Layout(absPath);
|
|
224
|
-
|
|
185
|
+
function readKdnaContainer(absPath, _opts) {
|
|
186
|
+
const v1Layout = require('./v1/index.js').readLayout(absPath);
|
|
225
187
|
const containerBuf = fs.readFileSync(absPath);
|
|
226
188
|
const assetDigest = crypto.createHash('sha256').update(containerBuf).digest();
|
|
227
189
|
|
|
228
190
|
return {
|
|
229
191
|
sourceKind: 'file',
|
|
230
192
|
sourcePath: absPath,
|
|
231
|
-
format: '
|
|
232
|
-
mimetype:
|
|
193
|
+
format: 'kdna',
|
|
194
|
+
mimetype: MIMETYPE,
|
|
233
195
|
manifest: v1Layout.manifest,
|
|
234
196
|
payloadRaw: v1Layout.map['payload.kdnab'] || null,
|
|
235
197
|
checksums: v1Layout.map['checksums.json']
|
|
@@ -241,58 +203,18 @@ function readV1Container(absPath, _opts) {
|
|
|
241
203
|
};
|
|
242
204
|
}
|
|
243
205
|
|
|
244
|
-
/**
|
|
245
|
-
* Read a Canonical Container (v2 mimetype) into CanonicalAssetModel.
|
|
246
|
-
* Uses the existing KdnaAssetReader for ZIP parsing.
|
|
247
|
-
*/
|
|
248
|
-
function readV2Container(absPath, _opts) {
|
|
249
|
-
const reader = createKdnaAssetReader();
|
|
250
|
-
const asset = reader.openSync(absPath);
|
|
251
|
-
|
|
252
|
-
const manifest = reader.readManifestSync(asset);
|
|
253
|
-
const payloadRaw = reader.readEntrySync(asset, 'payload.kdnab');
|
|
254
|
-
const checksums = (() => {
|
|
255
|
-
try {
|
|
256
|
-
const raw = reader.readEntrySync(asset, 'checksums.json');
|
|
257
|
-
return raw ? JSON.parse(raw.toString('utf8')) : null;
|
|
258
|
-
} catch {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
})();
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
sourceKind: 'file',
|
|
265
|
-
sourcePath: absPath,
|
|
266
|
-
format: 'v2-container',
|
|
267
|
-
mimetype: MIMETYPE_V2,
|
|
268
|
-
manifest,
|
|
269
|
-
payloadRaw,
|
|
270
|
-
checksums,
|
|
271
|
-
assetDigest: Buffer.from(asset.asset_digest.replace(/^sha256:/, ''), 'hex'),
|
|
272
|
-
containerBuffer: fs.readFileSync(absPath),
|
|
273
|
-
entries: null, // v2 uses lazy reads, not pre-loaded map
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Extract the mimetype from a CanonicalAssetModel.
|
|
279
|
-
*/
|
|
280
206
|
function getMimetype(asset) {
|
|
281
207
|
return asset.mimetype;
|
|
282
208
|
}
|
|
283
209
|
|
|
284
|
-
|
|
285
|
-
* Determine if a CanonicalAssetModel represents a source directory.
|
|
286
|
-
*/
|
|
287
|
-
function isSourceDirectory(asset) {
|
|
210
|
+
function isKdnaSourceDirectory(asset) {
|
|
288
211
|
return asset.sourceKind === 'dir';
|
|
289
212
|
}
|
|
290
213
|
|
|
291
214
|
module.exports = {
|
|
292
|
-
|
|
293
|
-
MIMETYPE_V2,
|
|
215
|
+
MIMETYPE,
|
|
294
216
|
readAsset,
|
|
295
217
|
detectFormat,
|
|
296
218
|
getMimetype,
|
|
297
|
-
|
|
219
|
+
isKdnaSourceDirectory,
|
|
298
220
|
};
|
package/src/crypto-profile.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
+
const cbor = require('cbor-x');
|
|
2
3
|
|
|
3
4
|
let argon2id;
|
|
4
5
|
try {
|
|
@@ -56,11 +57,13 @@ function decodeBase64(value, label) {
|
|
|
56
57
|
|
|
57
58
|
function normalizeEnvelope(value) {
|
|
58
59
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
59
|
-
return
|
|
60
|
+
try { return cbor.decode(Buffer.from(value)); } catch { /* fall through */ }
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'string') {
|
|
63
|
+
try { return cbor.decode(Buffer.from(value, 'utf8')); } catch { /* fall through */ }
|
|
60
64
|
}
|
|
61
|
-
if (typeof value === 'string') return JSON.parse(value);
|
|
62
65
|
if (value && typeof value === 'object') return value;
|
|
63
|
-
throw new Error('encrypted entry envelope must be
|
|
66
|
+
throw new Error('encrypted entry envelope must be CBOR (kdna_version 1.0)');
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
function encryptedEntryAad(entryName, manifest = {}, profile = LICENSED_ENTRY_PROFILE) {
|
package/src/index.mjs
CHANGED
|
@@ -12,6 +12,28 @@ export {
|
|
|
12
12
|
|
|
13
13
|
export { lintDomain } from './lint-pure.js';
|
|
14
14
|
|
|
15
|
+
export {
|
|
16
|
+
MIMETYPE,
|
|
17
|
+
REQUIRED_DIR_ENTRIES,
|
|
18
|
+
isKdnaSourceDir,
|
|
19
|
+
detectContainerFormat,
|
|
20
|
+
readLayout,
|
|
21
|
+
inspect,
|
|
22
|
+
validate,
|
|
23
|
+
planLoad,
|
|
24
|
+
loadAuthorized,
|
|
25
|
+
buildChecksums,
|
|
26
|
+
pack,
|
|
27
|
+
unpack,
|
|
28
|
+
load,
|
|
29
|
+
loadAsset,
|
|
30
|
+
buildCapsule,
|
|
31
|
+
FORBIDDEN_OUTPUT_TERMS,
|
|
32
|
+
parseSemver,
|
|
33
|
+
compareSemver,
|
|
34
|
+
satisfies,
|
|
35
|
+
} from './v1/index.js';
|
|
36
|
+
|
|
15
37
|
export { validateDomainSchema, validateCrossFile } from './validate-pure.js';
|
|
16
38
|
|
|
17
39
|
export { renderPreviewHTML, escHtml, renderCard } from './render.js';
|
package/src/lint-pure.js
CHANGED
|
@@ -273,13 +273,13 @@ const VALID_ASSET_TYPE = new Set([
|
|
|
273
273
|
]);
|
|
274
274
|
|
|
275
275
|
const MANIFEST_REQUIRED = [
|
|
276
|
-
'
|
|
276
|
+
'kdna_version', 'name', 'version',
|
|
277
277
|
'judgment_version', 'description', 'author', 'license', 'status',
|
|
278
278
|
'quality_badge', 'access', 'languages', 'default_language',
|
|
279
279
|
];
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
* Validate a kdna.json manifest against the canonical
|
|
282
|
+
* Validate a kdna.json manifest against the canonical KDNA Asset format.
|
|
283
283
|
*
|
|
284
284
|
* @param {Object} manifest — parsed kdna.json
|
|
285
285
|
* @returns {{ errors: string[], warnings: string[] }}
|
|
@@ -296,7 +296,7 @@ function validateManifest(manifest) {
|
|
|
296
296
|
// 1. Check disallowed pre-v1.0 manifest aliases
|
|
297
297
|
if ('kdna_spec' in manifest) {
|
|
298
298
|
errors.push(
|
|
299
|
-
'kdna.json: kdna_spec is not allowed. Use
|
|
299
|
+
'kdna.json: kdna_spec is not allowed. Use kdna_version.',
|
|
300
300
|
);
|
|
301
301
|
}
|
|
302
302
|
if ('language' in manifest) {
|
|
@@ -319,9 +319,11 @@ function validateManifest(manifest) {
|
|
|
319
319
|
if (manifest.format && manifest.format !== 'kdna') {
|
|
320
320
|
errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
|
|
321
321
|
}
|
|
322
|
-
if (manifest.
|
|
322
|
+
if (!manifest.kdna_version) {
|
|
323
|
+
errors.push('kdna.json: missing required field "kdna_version"');
|
|
324
|
+
} else if (manifest.kdna_version !== '1.0') {
|
|
323
325
|
errors.push(
|
|
324
|
-
`kdna.json.
|
|
326
|
+
`kdna.json.kdna_version: invalid value "${manifest.kdna_version}". Expected "1.0".`,
|
|
325
327
|
);
|
|
326
328
|
}
|
|
327
329
|
if (manifest.status && !VALID_STATUS.has(manifest.status)) {
|
|
@@ -394,14 +396,7 @@ function validateManifest(manifest) {
|
|
|
394
396
|
errors.push('kdna.json.license: missing "type"');
|
|
395
397
|
}
|
|
396
398
|
|
|
397
|
-
// 9. Validate
|
|
398
|
-
if (manifest.spec_version && manifest.spec_version !== '1.0-rc') {
|
|
399
|
-
warnings.push(
|
|
400
|
-
`kdna.json.spec_version: non-standard value "${manifest.spec_version}". Expected "1.0-rc".`,
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 10. Validate version format
|
|
399
|
+
// 9. Validate version format
|
|
405
400
|
if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
|
406
401
|
warnings.push(
|
|
407
402
|
`kdna.json.version: non-semver format "${manifest.version}". Expected MAJOR.MINOR.PATCH.`,
|
package/src/types.d.ts
CHANGED
|
@@ -203,9 +203,7 @@ export interface KDNAFileDataMap {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
export interface KDNAManifest {
|
|
206
|
-
|
|
207
|
-
format_version: '2.0';
|
|
208
|
-
spec_version: string;
|
|
206
|
+
kdna_version: '1.0';
|
|
209
207
|
name: string;
|
|
210
208
|
version: string;
|
|
211
209
|
judgment_version: string;
|
|
@@ -484,36 +482,33 @@ export function matchDomain(input: string, candidates: Array<KDNAAssetInput | KD
|
|
|
484
482
|
export function matchDomainSync(input: string, candidates: Array<KDNAAssetInput | KDNAInspectResult>, options?: KdnaDecryptOptions): KDNAMatchResult[];
|
|
485
483
|
export function composeKDNA(inputs: KDNAAssetInput[], options?: { input?: string; profile?: 'compact' | 'scenario' | 'full' | string; separator?: string } & KdnaDecryptOptions): Promise<KDNAComposeResult>;
|
|
486
484
|
|
|
487
|
-
// KDNA Core
|
|
485
|
+
// KDNA Core — source directory / container API
|
|
488
486
|
export const MIMETYPE: string;
|
|
489
|
-
export const
|
|
490
|
-
export const MIMETYPE_V2: string;
|
|
491
|
-
export const V1_REQUIRED_DIR_ENTRIES: string[];
|
|
487
|
+
export const REQUIRED_DIR_ENTRIES: string[];
|
|
492
488
|
|
|
493
|
-
export interface
|
|
489
|
+
export interface KDNAChecksumEntry {
|
|
494
490
|
algorithm: 'sha256';
|
|
495
491
|
value: string;
|
|
496
492
|
}
|
|
497
493
|
|
|
498
|
-
export interface
|
|
494
|
+
export interface KDNAChecksums {
|
|
499
495
|
algorithm: 'sha256';
|
|
500
496
|
manifest_digest: string;
|
|
501
497
|
payload_digest: string;
|
|
502
498
|
asset_digest: string;
|
|
503
|
-
entries: Record<string,
|
|
499
|
+
entries: Record<string, KDNAChecksumEntry>;
|
|
504
500
|
}
|
|
505
501
|
|
|
506
|
-
export function
|
|
507
|
-
export function
|
|
508
|
-
export function detectContainerFormat(inputPath: string): 'v1' | 'v2' | null;
|
|
502
|
+
export function isKdnaSourceDir(inputPath: string): boolean;
|
|
503
|
+
export function detectContainerFormat(inputPath: string): 'kdna' | null;
|
|
509
504
|
export function inspect(inputPath: string, options?: Record<string, any>): Record<string, any>;
|
|
510
505
|
export function validate(inputPath: string, options?: Record<string, any>): Record<string, any>;
|
|
511
|
-
export interface
|
|
506
|
+
export interface KDNALoadPlanIssue {
|
|
512
507
|
code: string;
|
|
513
508
|
severity: 'info' | 'warning' | 'blocking' | string;
|
|
514
509
|
message: string;
|
|
515
510
|
}
|
|
516
|
-
export interface
|
|
511
|
+
export interface KDNALoadPlan {
|
|
517
512
|
kdna_version: string | null;
|
|
518
513
|
asset: {
|
|
519
514
|
asset_id: string | null;
|
|
@@ -552,18 +547,19 @@ export interface KDNAV1LoadPlan {
|
|
|
552
547
|
projection_policy: 'minimal' | 'remote' | 'none' | string;
|
|
553
548
|
input_fingerprint: string | null;
|
|
554
549
|
checks: Record<string, boolean>;
|
|
555
|
-
issues:
|
|
550
|
+
issues: KDNALoadPlanIssue[];
|
|
556
551
|
source: {
|
|
557
552
|
kind: 'dir' | 'file' | string | null;
|
|
558
553
|
path: string;
|
|
559
554
|
};
|
|
560
555
|
}
|
|
561
|
-
export function planLoad(inputPath: string, options?: { password?: string; hasPassword?: boolean; entitlement?: Record<string, any> }):
|
|
562
|
-
export function
|
|
556
|
+
export function planLoad(inputPath: string, options?: { password?: string; hasPassword?: boolean; entitlement?: Record<string, any> }): KDNALoadPlan;
|
|
557
|
+
export function buildChecksums(sourceDir: string): KDNAChecksums;
|
|
563
558
|
export function pack(sourceDir: string, outputPath: string): void;
|
|
564
559
|
export function unpack(inputPath: string, outputDir: string): void;
|
|
565
560
|
export function loadAuthorized(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
|
|
566
|
-
export function
|
|
561
|
+
export function load(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
|
|
562
|
+
export function loadAsset(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
|
|
567
563
|
export const FORBIDDEN_OUTPUT_TERMS: readonly string[];
|
|
568
564
|
|
|
569
565
|
// Lint
|
package/src/v1/index.js
CHANGED
|
@@ -38,24 +38,24 @@ const fs = require('node:fs');
|
|
|
38
38
|
const path = require('node:path');
|
|
39
39
|
const zlib = require('node:zlib');
|
|
40
40
|
const crypto = require('node:crypto');
|
|
41
|
+
const cbor = require('cbor-x');
|
|
41
42
|
|
|
42
43
|
const { readAsset } = (() => {
|
|
43
44
|
try {
|
|
44
45
|
return require('../container-dispatcher.js');
|
|
45
46
|
} catch {
|
|
46
|
-
return { readAsset: null
|
|
47
|
+
return { readAsset: null };
|
|
47
48
|
}
|
|
48
49
|
})();
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
...
|
|
56
|
-
...V1_OPTIONAL_DIR_ENTRIES,
|
|
51
|
+
const MIMETYPE = 'application/vnd.kdna.asset';
|
|
52
|
+
const REQUIRED_DIR_ENTRIES = ['mimetype', 'kdna.json', 'payload.kdnab'];
|
|
53
|
+
const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'signatures', 'attachments'];
|
|
54
|
+
const ALLOWED_TOP_LEVEL_ENTRIES = new Set([
|
|
55
|
+
...REQUIRED_DIR_ENTRIES,
|
|
56
|
+
...OPTIONAL_DIR_ENTRIES,
|
|
57
57
|
]);
|
|
58
|
-
const
|
|
58
|
+
const FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
|
|
59
59
|
'KDNA_Core.json',
|
|
60
60
|
'KDNA_Patterns.json',
|
|
61
61
|
'KDNA_Scenarios.json',
|
|
@@ -64,6 +64,12 @@ const V1_FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
|
|
|
64
64
|
'KDNA_Evolution.json',
|
|
65
65
|
]);
|
|
66
66
|
|
|
67
|
+
// Legacy aliases — prefer the unversioned names above
|
|
68
|
+
const V1_REQUIRED_DIR_ENTRIES = REQUIRED_DIR_ENTRIES;
|
|
69
|
+
const V1_OPTIONAL_DIR_ENTRIES = OPTIONAL_DIR_ENTRIES;
|
|
70
|
+
const V1_ALLOWED_TOP_LEVEL_ENTRIES = ALLOWED_TOP_LEVEL_ENTRIES;
|
|
71
|
+
const V1_FORBIDDEN_LEGACY_TOP_LEVEL = FORBIDDEN_LEGACY_TOP_LEVEL;
|
|
72
|
+
|
|
67
73
|
const DEFAULT_CONTAINER_LIMITS = Object.freeze({
|
|
68
74
|
maxContainerBytes: 25 * 1024 * 1024,
|
|
69
75
|
maxEntries: 128,
|
|
@@ -176,45 +182,28 @@ function loadSchemas() {
|
|
|
176
182
|
* Required entries: mimetype, kdna.json, payload.kdnab.
|
|
177
183
|
* mimetype content must equal "application/vnd.kdna.asset".
|
|
178
184
|
*/
|
|
179
|
-
function
|
|
185
|
+
function isKdnaSourceDir(absPath) {
|
|
180
186
|
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) return false;
|
|
181
187
|
for (const f of V1_REQUIRED_DIR_ENTRIES) {
|
|
182
188
|
if (!fs.existsSync(path.join(absPath, f))) return false;
|
|
183
189
|
}
|
|
184
190
|
const mime = fs.readFileSync(path.join(absPath, 'mimetype'), 'utf8');
|
|
185
|
-
return mime ===
|
|
191
|
+
return mime === MIMETYPE;
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
/**
|
|
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
195
|
/**
|
|
203
|
-
* Detect whether a file is a
|
|
204
|
-
* Returns '
|
|
196
|
+
* Detect whether a file is a .kdna container.
|
|
197
|
+
* Returns 'kdna' | null. null = not a .kdna file or unreadable.
|
|
205
198
|
*/
|
|
206
199
|
function detectContainerFormat(absPath) {
|
|
207
200
|
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) return null;
|
|
208
|
-
// Quick header check: must look like a ZIP.
|
|
209
201
|
const fd = fs.openSync(absPath, 'r');
|
|
210
202
|
const head = Buffer.alloc(4);
|
|
211
203
|
fs.readSync(fd, head, 0, 4, 0);
|
|
212
204
|
fs.closeSync(fd);
|
|
213
205
|
if (head[0] !== 0x50 || head[1] !== 0x4b) return null;
|
|
214
206
|
|
|
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
207
|
let first;
|
|
219
208
|
try {
|
|
220
209
|
first = readFirstZipEntry(absPath);
|
|
@@ -222,12 +211,9 @@ function detectContainerFormat(absPath) {
|
|
|
222
211
|
return null;
|
|
223
212
|
}
|
|
224
213
|
if (first.name !== 'mimetype') return null;
|
|
225
|
-
// The mimetype entry must be STORED (method 0).
|
|
226
214
|
if (first.method !== 0) return null;
|
|
227
215
|
const mime = first.method === 0 ? first.data.toString('utf8') : '';
|
|
228
|
-
|
|
229
|
-
if (mime === MIMETYPE_V2) return 'v2';
|
|
230
|
-
return null;
|
|
216
|
+
return (mime === MIMETYPE) ? 'kdna' : null;
|
|
231
217
|
}
|
|
232
218
|
|
|
233
219
|
// ─── ZIP I/O ────────────────────────────────────────────────────────────
|
|
@@ -599,9 +585,9 @@ function readV1Layout(absPath) {
|
|
|
599
585
|
|
|
600
586
|
// mimetype content must equal the literal v1 media type.
|
|
601
587
|
const mime = map.mimetype.toString('utf8');
|
|
602
|
-
if (mime !==
|
|
588
|
+
if (mime !== MIMETYPE) {
|
|
603
589
|
throw new Error(
|
|
604
|
-
`not a KDNA layout: mimetype is "${mime}", expected "${
|
|
590
|
+
`not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE}"`,
|
|
605
591
|
);
|
|
606
592
|
}
|
|
607
593
|
|
|
@@ -657,6 +643,24 @@ function assertJsonWithinLimits(value, name, limits) {
|
|
|
657
643
|
walk(value, 0);
|
|
658
644
|
}
|
|
659
645
|
|
|
646
|
+
function parsePayloadEntry(name, bytes, opts = {}) {
|
|
647
|
+
if (!Buffer.isBuffer(bytes)) {
|
|
648
|
+
throw new Error(`${name} is not a file entry`);
|
|
649
|
+
}
|
|
650
|
+
const limits = { ...DEFAULT_CONTAINER_LIMITS, ...(opts.limits || {}) };
|
|
651
|
+
if (bytes.length > limits.maxEntryBytes) {
|
|
652
|
+
throw new Error(`${name} exceeds maximum size (${limits.maxEntryBytes} bytes)`);
|
|
653
|
+
}
|
|
654
|
+
let parsed;
|
|
655
|
+
try {
|
|
656
|
+
parsed = cbor.decode(bytes);
|
|
657
|
+
} catch {
|
|
658
|
+
throw new Error(`${name}: not valid CBOR. payload.kdnab must be CBOR-encoded (kdna_version 1.0).`);
|
|
659
|
+
}
|
|
660
|
+
assertJsonWithinLimits(parsed, name, limits);
|
|
661
|
+
return parsed;
|
|
662
|
+
}
|
|
663
|
+
|
|
660
664
|
// ─── inspect ───────────────────────────────────────────────────────────
|
|
661
665
|
|
|
662
666
|
/**
|
|
@@ -710,9 +714,9 @@ function runValidate(v1) {
|
|
|
710
714
|
}
|
|
711
715
|
if (v1.map.mimetype) {
|
|
712
716
|
const mimeStr = v1.map.mimetype.toString('utf8');
|
|
713
|
-
if (mimeStr !==
|
|
717
|
+
if (mimeStr !== MIMETYPE) {
|
|
714
718
|
result.format_valid = false;
|
|
715
|
-
problems.push(`format: mimetype is not ${
|
|
719
|
+
problems.push(`format: mimetype is not ${MIMETYPE}`);
|
|
716
720
|
}
|
|
717
721
|
}
|
|
718
722
|
|
|
@@ -736,10 +740,10 @@ function runValidate(v1) {
|
|
|
736
740
|
// For actually encrypted payloads (envelopes), skip schema validation.
|
|
737
741
|
let payload;
|
|
738
742
|
try {
|
|
739
|
-
payload =
|
|
743
|
+
payload = parsePayloadEntry("payload.kdnab", v1.map['payload.kdnab']);
|
|
740
744
|
} catch (e) {
|
|
741
745
|
result.payload_valid = false;
|
|
742
|
-
problems.push(`payload: not valid
|
|
746
|
+
problems.push(`payload: not valid KDNA payload (${e.message})`);
|
|
743
747
|
return finalizeValidate(result, problems);
|
|
744
748
|
}
|
|
745
749
|
|
|
@@ -931,12 +935,22 @@ function pack(sourceDir, outputPath) {
|
|
|
931
935
|
}
|
|
932
936
|
}
|
|
933
937
|
const mime = fs.readFileSync(path.join(absSrc, 'mimetype'), 'utf8');
|
|
934
|
-
if (mime !==
|
|
935
|
-
throw new Error(`cannot pack: mimetype is "${mime}", expected "${
|
|
938
|
+
if (mime !== MIMETYPE) {
|
|
939
|
+
throw new Error(`cannot pack: mimetype is "${mime}", expected "${MIMETYPE}"`);
|
|
936
940
|
}
|
|
937
941
|
|
|
938
942
|
// Collect deterministically; mimetype is forced first.
|
|
939
943
|
const collected = listSourceDir(absSrc);
|
|
944
|
+
for (const entry of collected) {
|
|
945
|
+
const normalizedName = normalizeContainerEntryName(entry.rel);
|
|
946
|
+
const topLevel = normalizedName.split('/')[0];
|
|
947
|
+
if (!ALLOWED_TOP_LEVEL_ENTRIES.has(topLevel)) {
|
|
948
|
+
if (FORBIDDEN_LEGACY_TOP_LEVEL.has(topLevel)) {
|
|
949
|
+
throw new Error(`cannot pack forbidden top-level source entry: ${topLevel}`);
|
|
950
|
+
}
|
|
951
|
+
throw new Error(`cannot pack unsupported top-level entry: ${topLevel}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
940
954
|
const order = ['mimetype', ...collected.map((e) => e.rel).filter((n) => n !== 'mimetype')];
|
|
941
955
|
|
|
942
956
|
// Build the ZIP body.
|
|
@@ -946,7 +960,7 @@ function pack(sourceDir, outputPath) {
|
|
|
946
960
|
for (const rel of order) {
|
|
947
961
|
let data;
|
|
948
962
|
if (rel === 'mimetype') {
|
|
949
|
-
data = Buffer.from(
|
|
963
|
+
data = Buffer.from(MIMETYPE, 'utf8');
|
|
950
964
|
} else {
|
|
951
965
|
const found = collected.find((e) => e.rel === rel);
|
|
952
966
|
if (!found) continue;
|
|
@@ -1011,9 +1025,9 @@ function unpack(inputPath, outputDir) {
|
|
|
1011
1025
|
throw new Error('not a KDNA container: mimetype must be uncompressed');
|
|
1012
1026
|
}
|
|
1013
1027
|
const mime = entries[0].data.toString('utf8');
|
|
1014
|
-
if (mime !==
|
|
1028
|
+
if (mime !== MIMETYPE) {
|
|
1015
1029
|
throw new Error(
|
|
1016
|
-
`not a KDNA container: mimetype is "${mime}", expected "${
|
|
1030
|
+
`not a KDNA container: mimetype is "${mime}", expected "${MIMETYPE}"`,
|
|
1017
1031
|
);
|
|
1018
1032
|
}
|
|
1019
1033
|
const absOut = path.resolve(outputDir);
|
|
@@ -1621,26 +1635,22 @@ function assertNoForbiddenTerms(obj) {
|
|
|
1621
1635
|
}
|
|
1622
1636
|
|
|
1623
1637
|
module.exports = {
|
|
1624
|
-
MIMETYPE
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
V1_REQUIRED_DIR_ENTRIES,
|
|
1628
|
-
isV1SourceDir,
|
|
1629
|
-
isV2SourceDir,
|
|
1638
|
+
MIMETYPE,
|
|
1639
|
+
REQUIRED_DIR_ENTRIES,
|
|
1640
|
+
isKdnaSourceDir,
|
|
1630
1641
|
detectContainerFormat,
|
|
1631
|
-
readV1Layout,
|
|
1642
|
+
readLayout: readV1Layout,
|
|
1632
1643
|
inspect,
|
|
1633
1644
|
validate,
|
|
1634
1645
|
planLoad,
|
|
1635
1646
|
loadAuthorized,
|
|
1636
|
-
buildChecksumsV1,
|
|
1647
|
+
buildChecksums: buildChecksumsV1,
|
|
1637
1648
|
pack,
|
|
1638
1649
|
unpack,
|
|
1639
|
-
loadV1,
|
|
1650
|
+
load: loadV1,
|
|
1651
|
+
loadAsset: loadV1,
|
|
1652
|
+
buildCapsule,
|
|
1640
1653
|
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
1654
|
parseSemver,
|
|
1645
1655
|
compareSemver,
|
|
1646
1656
|
satisfies,
|
|
@@ -1742,8 +1752,11 @@ function loadAuthorized(inputPath, opts = {}) {
|
|
|
1742
1752
|
const mergedOpts = {
|
|
1743
1753
|
...opts,
|
|
1744
1754
|
resolvedDependencies: plan.resolved_dependencies,
|
|
1745
|
-
// Pass extends_chain from plan to loadV1Unsafe so inheritance is applied (Story 12)
|
|
1746
1755
|
extendsChain: plan.extends_chain || [],
|
|
1756
|
+
_validation: {
|
|
1757
|
+
schema_valid: plan.checks && plan.checks.overall_valid === true,
|
|
1758
|
+
signature_valid: plan.signature_valid === true,
|
|
1759
|
+
},
|
|
1747
1760
|
};
|
|
1748
1761
|
return loadV1Unsafe(inputPath, mergedOpts);
|
|
1749
1762
|
}
|
|
@@ -1793,9 +1806,9 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1793
1806
|
let payload;
|
|
1794
1807
|
try {
|
|
1795
1808
|
const rawPayload = v1.map['payload.kdnab'];
|
|
1796
|
-
payload =
|
|
1809
|
+
payload = parsePayloadEntry("payload.kdnab", rawPayload);
|
|
1797
1810
|
} catch (e) {
|
|
1798
|
-
throw new Error(`payload.kdnab is not valid
|
|
1811
|
+
throw new Error(`payload.kdnab is not valid KDNA payload: ${e.message}`, { cause: e });
|
|
1799
1812
|
}
|
|
1800
1813
|
|
|
1801
1814
|
if (payload.profile && payload.ciphertext && (m.payload?.encrypted || m.encryption?.encrypted_entries?.includes('payload.kdnab'))) {
|
|
@@ -1812,7 +1825,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1812
1825
|
const decryptedBuf = typeof decrypted === 'string'
|
|
1813
1826
|
? Buffer.from(decrypted, 'utf8')
|
|
1814
1827
|
: Buffer.from(decrypted);
|
|
1815
|
-
payload =
|
|
1828
|
+
payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
|
|
1816
1829
|
} catch (e) {
|
|
1817
1830
|
const err = new Error(`Decryption failed: ${e.message}`);
|
|
1818
1831
|
err.code = 'KDNA_DECRYPT_FAILED';
|
|
@@ -1827,8 +1840,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1827
1840
|
decryptProtectedEntryScrypt,
|
|
1828
1841
|
PASSWORD_PROTECTED_SCRYPT_PROFILE,
|
|
1829
1842
|
} = require('../crypto-profile.js');
|
|
1830
|
-
const encryptedEnvelope = v1.map['payload.kdnab']
|
|
1831
|
-
|
|
1843
|
+
const encryptedEnvelope = v1.map['payload.kdnab'];
|
|
1844
|
+
let envelope;
|
|
1845
|
+
try { envelope = cbor.decode(Buffer.isBuffer(encryptedEnvelope) ? encryptedEnvelope : Buffer.from(encryptedEnvelope)); }
|
|
1846
|
+
catch { throw new Error('Cannot decode encrypted envelope: payload.kdnab must be CBOR-encoded (kdna_version 1.0)'); }
|
|
1832
1847
|
const decryptFn = (envelope.profile === PASSWORD_PROTECTED_SCRYPT_PROFILE)
|
|
1833
1848
|
? decryptProtectedEntryScrypt
|
|
1834
1849
|
: decryptProtectedEntry;
|
|
@@ -1837,7 +1852,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1837
1852
|
manifest: m,
|
|
1838
1853
|
password: opts.password,
|
|
1839
1854
|
});
|
|
1840
|
-
payload =
|
|
1855
|
+
payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
|
|
1841
1856
|
} catch (e) {
|
|
1842
1857
|
const err = new Error(`Decryption failed with provided password: ${e.message}`);
|
|
1843
1858
|
err.code = 'KDNA_DECRYPT_FAILED';
|
|
@@ -1938,8 +1953,9 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1938
1953
|
resolvedDependencies: [],
|
|
1939
1954
|
extendsChain: [],
|
|
1940
1955
|
});
|
|
1941
|
-
|
|
1942
|
-
|
|
1956
|
+
const baseContent = baseLoaded && (baseLoaded.content || baseLoaded.context);
|
|
1957
|
+
if (baseContent) {
|
|
1958
|
+
const bc = baseContent;
|
|
1943
1959
|
const cc = result.content;
|
|
1944
1960
|
// Merge axioms: child overrides by id; parent axioms not in child are added
|
|
1945
1961
|
if (bc.axioms || cc.axioms) {
|
|
@@ -2048,9 +2064,65 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2048
2064
|
return { status: result.status, profile: result.profile, text: textOut };
|
|
2049
2065
|
}
|
|
2050
2066
|
|
|
2067
|
+
if (as === 'prompt') {
|
|
2068
|
+
return result;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
return wrapAsCapsule(result, v1, profile, opts);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function wrapAsCapsule(result, v1, profile, opts) {
|
|
2075
|
+
if (opts.as === 'json') {
|
|
2076
|
+
return buildCapsule(result, v1, profile, opts);
|
|
2077
|
+
}
|
|
2051
2078
|
return result;
|
|
2052
2079
|
}
|
|
2053
2080
|
|
|
2081
|
+
function buildCapsule(loadResult, v1, profile, opts = {}) {
|
|
2082
|
+
const m = v1.manifest;
|
|
2083
|
+
const val = opts._validation || {};
|
|
2084
|
+
const assetDigest = v1.assetDigest || (v1.map && v1.map['checksums.json']
|
|
2085
|
+
? (() => {
|
|
2086
|
+
try {
|
|
2087
|
+
const checks = JSON.parse(v1.map['checksums.json'].toString('utf8'));
|
|
2088
|
+
return checks.asset_digest || null;
|
|
2089
|
+
} catch { return null; }
|
|
2090
|
+
})()
|
|
2091
|
+
: null);
|
|
2092
|
+
|
|
2093
|
+
const pubkey = (m.creator && m.creator.pubkey) || (m.author && m.author.pubkey) || null;
|
|
2094
|
+
const sigVerified = val.signature_valid === true;
|
|
2095
|
+
const capsule = {
|
|
2096
|
+
type: 'kdna.context.capsule',
|
|
2097
|
+
version: '1.0',
|
|
2098
|
+
domain: m.name || m.asset_id || null,
|
|
2099
|
+
judgment_version: m.judgment_version || null,
|
|
2100
|
+
asset_digest: assetDigest,
|
|
2101
|
+
signature: pubkey
|
|
2102
|
+
? { state: sigVerified ? 'verified' : 'not_checked', issuer: pubkey }
|
|
2103
|
+
: { state: 'absent' },
|
|
2104
|
+
access: m.access || 'public',
|
|
2105
|
+
risk_level: m.risk_level || null,
|
|
2106
|
+
profile: loadResult.profile,
|
|
2107
|
+
context: loadResult.content || {},
|
|
2108
|
+
trace: {
|
|
2109
|
+
payload_encoding: (m.payload && m.payload.encoding) || 'cbor',
|
|
2110
|
+
loaded_by: 'kdna-core',
|
|
2111
|
+
loaded_at: new Date().toISOString(),
|
|
2112
|
+
schema_valid: val.schema_valid === true,
|
|
2113
|
+
signature_state: sigVerified ? 'verified' : (pubkey ? 'not_checked' : 'absent'),
|
|
2114
|
+
profile: profile,
|
|
2115
|
+
},
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
if (loadResult.extends_chain) capsule.extends_chain = loadResult.extends_chain;
|
|
2119
|
+
if (loadResult.inheritance_applied !== undefined) capsule.inheritance_applied = loadResult.inheritance_applied;
|
|
2120
|
+
if (loadResult.resolved_dependencies) capsule.resolved_dependencies = loadResult.resolved_dependencies;
|
|
2121
|
+
if (loadResult.rag_isolation_policy) capsule.rag_isolation_policy = loadResult.rag_isolation_policy;
|
|
2122
|
+
|
|
2123
|
+
return capsule;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2054
2126
|
// ─── Semver & Dependency Resolution (Story 6) ─────────────────────────
|
|
2055
2127
|
|
|
2056
2128
|
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;
|