@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.16.0 (2026-07-13)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- RFC-0019 external account/device key grants, schemas, deterministic golden
|
|
8
|
+
and negative fixtures, and JS/Swift parity vectors.
|
|
9
|
+
- JS Core helpers for external envelope encryption, per-device grant issuance,
|
|
10
|
+
signature/binding verification, in-memory decryption, and explicit zeroization.
|
|
11
|
+
- LoadPlan accepts only a runtime-verified external entitlement for account/org
|
|
12
|
+
assets; structurally similar status objects no longer authorize loading.
|
|
13
|
+
- Reject authoring source directories at the public runtime API boundary;
|
|
14
|
+
`planLoad`, `load`, `loadAsset`, and `loadAuthorized` now require a packaged
|
|
15
|
+
`.kdna` file, including resolver-provided dependencies.
|
|
16
|
+
- Regenerate authorization conformance fixtures from packaged assets and keep
|
|
17
|
+
source directories available only to authoring operations such as inspect,
|
|
18
|
+
validate, pack, and unpack.
|
|
19
|
+
- Remove stale public format-generation wording and local coordination-path
|
|
20
|
+
assumptions from protocol documentation and public audit scripts.
|
|
21
|
+
- Make release readiness fail for dirty inputs or when the selected version
|
|
22
|
+
tag does not point to the current commit.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- Account/device envelopes never fall back to the password profile.
|
|
27
|
+
- Revoked, expired, tampered, digest/version/account/device mismatches fail
|
|
28
|
+
closed before Runtime Capsule projection.
|
|
29
|
+
|
|
30
|
+
## v0.15.12 (2026-07-12)
|
|
31
|
+
|
|
32
|
+
**Single-format refactor + CBOR wire contract + Runtime Capsule.**
|
|
33
|
+
|
|
34
|
+
### Breaking changes
|
|
35
|
+
|
|
36
|
+
- `MIMETYPE_V1`, `MIMETYPE_V2`, `MIMETYPE_LEGACY`, `isV1SourceDir`, `isV2SourceDir`
|
|
37
|
+
removed from public API. Use `MIMETYPE`, `isKdnaSourceDir`, `detectContainerFormat`.
|
|
38
|
+
- `detectContainerFormat` returns only `"kdna"` or `null`.
|
|
39
|
+
- `kdna_version: "2.0"` is no longer accepted. Only `"1.0"`.
|
|
40
|
+
- `payload.encoding` accepts only `"cbor"`. JSON payload bytes are rejected;
|
|
41
|
+
there is no production JSON fallback.
|
|
42
|
+
- Version-qualified public APIs (`V1_REQUIRED_DIR_ENTRIES`, `readV1Layout`,
|
|
43
|
+
`buildChecksumsV1`, `loadV1`, and the `./v1` export) are removed. Use the
|
|
44
|
+
unversioned APIs below.
|
|
45
|
+
- `kdna load --as=json` now returns Runtime Capsule format
|
|
46
|
+
(`type: "kdna.context.capsule"`) instead of raw `{status, content}`.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- `REQUIRED_DIR_ENTRIES`, `readLayout`, `buildChecksums`, `load`, and
|
|
51
|
+
`loadAsset` are the sole public container/loading APIs.
|
|
52
|
+
- `buildCapsule(loadResult, v1, profile, opts)` — wraps load output as
|
|
53
|
+
`kdna.context.capsule` per `specs/runtime-capsule.md`.
|
|
54
|
+
- `parsePayloadEntry` — strict CBOR decoding for `payload.kdnab`.
|
|
55
|
+
|
|
56
|
+
### Fixes
|
|
57
|
+
|
|
58
|
+
- Producer/consumer encoding alignment: `dev-pack.js`, `studio.js` write CBOR;
|
|
59
|
+
validation and loading decode CBOR, including encrypted envelopes.
|
|
60
|
+
- `loadV1Unsafe` extends/deps processing occurs before Capsule wrapping (Story 12 fix).
|
|
61
|
+
- All test fixtures converted to CBOR; golden files regenerated.
|
|
62
|
+
|
|
3
63
|
## v0.15.11 (2026-07-03)
|
|
4
64
|
|
|
5
65
|
**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,22 @@ 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
|
+
|
|
100
|
+
### Account/device external grants
|
|
101
|
+
|
|
102
|
+
RFC-0019 licensed assets can keep the encrypted container publicly available
|
|
103
|
+
while an issuer grants independent account/device access. `authorizeExternalKeyGrant()`
|
|
104
|
+
verifies the issuer signature, lease, revocation state, account, device, asset
|
|
105
|
+
UID/version/digest, and encrypted entry before returning a branded entitlement
|
|
106
|
+
and in-memory decrypt hook. A plain `{status: "active"}` object is not accepted.
|
|
107
|
+
|
|
108
|
+
Device private keys belong in the platform SecretStore. The returned session
|
|
109
|
+
must be disposed after loading; Agent-facing callers should receive only the
|
|
110
|
+
Runtime Capsule. Account grants never fall back to the password profile.
|
|
111
|
+
|
|
91
112
|
## Load Profiles
|
|
92
113
|
|
|
93
114
|
`loadAuthorized()` supports:
|
|
@@ -104,15 +125,9 @@ Output formats:
|
|
|
104
125
|
|
|
105
126
|
## Boundary
|
|
106
127
|
|
|
107
|
-
KDNA Core
|
|
128
|
+
KDNA Core is content-neutral. It validates file structure and loadability;
|
|
108
129
|
it does not rank judgment quality or endorse specific assets.
|
|
109
130
|
|
|
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
131
|
## License
|
|
117
132
|
|
|
118
133
|
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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": {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://kdna.ai/specs/kdna-envelope-external-grant-v1.schema.json",
|
|
4
|
+
"title": "KDNA external grant envelope v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["profile", "alg", "cek_derivation", "key_ref", "issuer_key_id", "entry_path", "plaintext_digest", "iv", "tag", "ciphertext"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"profile": { "const": "kdna-envelope-external-grant-v1" },
|
|
10
|
+
"alg": { "const": "A256GCM" },
|
|
11
|
+
"cek_derivation": { "const": "HKDF-SHA256" },
|
|
12
|
+
"key_ref": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
13
|
+
"issuer_key_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
14
|
+
"entry_path": { "type": "string", "pattern": "^[^/].*[^/]$|^[^/]$", "maxLength": 256 },
|
|
15
|
+
"plaintext_digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
|
|
16
|
+
"iv": { "type": "string", "pattern": "^[A-Za-z0-9_-]{16}$" },
|
|
17
|
+
"tag": { "type": "string", "pattern": "^[A-Za-z0-9_-]{22}$" },
|
|
18
|
+
"ciphertext": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_-]+$" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://kdna.ai/specs/kdna-key-grant-v1.schema.json",
|
|
4
|
+
"title": "KDNA account/device external key grant v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["profile", "grant_id", "issuer", "signing_key_id", "entitlement_id", "account_id", "device_id", "device_public_key", "device_signing_public_key", "asset", "issued_at", "refresh_after", "offline_grace_until", "expires_at", "status", "status_version", "wrap", "signature"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"profile": { "const": "kdna-key-grant-v1" },
|
|
10
|
+
"grant_id": { "$ref": "#/$defs/id" },
|
|
11
|
+
"issuer": { "type": "string", "format": "uri", "maxLength": 256 },
|
|
12
|
+
"signing_key_id": { "$ref": "#/$defs/id" },
|
|
13
|
+
"entitlement_id": { "$ref": "#/$defs/id" },
|
|
14
|
+
"account_id": { "$ref": "#/$defs/id" },
|
|
15
|
+
"device_id": { "$ref": "#/$defs/id" },
|
|
16
|
+
"device_public_key": { "type": "string", "pattern": "^x25519:[A-Za-z0-9_-]{43}$" },
|
|
17
|
+
"device_signing_public_key": { "type": "string", "pattern": "^ed25519:[A-Za-z0-9_-]{43}$" },
|
|
18
|
+
"asset": {
|
|
19
|
+
"type": "object", "additionalProperties": false,
|
|
20
|
+
"required": ["asset_id", "asset_uid", "version", "digest", "entry_path", "ciphertext_digest", "key_ref", "issuer_key_id"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"asset_id": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
23
|
+
"asset_uid": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
24
|
+
"version": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
25
|
+
"digest": { "$ref": "#/$defs/digest" },
|
|
26
|
+
"entry_path": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
27
|
+
"ciphertext_digest": { "$ref": "#/$defs/digest" },
|
|
28
|
+
"key_ref": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
29
|
+
"issuer_key_id": { "$ref": "#/$defs/id" }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"issued_at": { "type": "string", "format": "date-time" },
|
|
33
|
+
"refresh_after": { "type": "string", "format": "date-time" },
|
|
34
|
+
"offline_grace_until": { "type": "string", "format": "date-time" },
|
|
35
|
+
"expires_at": { "type": "string", "format": "date-time" },
|
|
36
|
+
"status": { "enum": ["active", "revoked", "expired"] },
|
|
37
|
+
"status_version": { "type": "integer", "minimum": 1 },
|
|
38
|
+
"wrap": {
|
|
39
|
+
"type": "object", "additionalProperties": false,
|
|
40
|
+
"required": ["alg", "ephemeral_public_key", "salt", "wrapped_cek"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"alg": { "const": "X25519-HKDF-SHA256+A256KW" },
|
|
43
|
+
"ephemeral_public_key": { "type": "string", "pattern": "^x25519:[A-Za-z0-9_-]{43}$" },
|
|
44
|
+
"salt": { "type": "string", "pattern": "^[A-Za-z0-9_-]{22}$" },
|
|
45
|
+
"wrapped_cek": { "type": "string", "pattern": "^[A-Za-z0-9_-]{54}$" }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"signature": { "type": "string", "pattern": "^ed25519:[A-Za-z0-9_-]{86}$" }
|
|
49
|
+
},
|
|
50
|
+
"$defs": {
|
|
51
|
+
"id": { "type": "string", "pattern": "^[A-Za-z0-9._:@/-]{1,256}$" },
|
|
52
|
+
"digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/aikdna/kdna/schema/load-plan.schema.json",
|
|
4
4
|
"title": "KDNA LoadPlan v1",
|
|
5
|
-
"description": "Machine-readable pre-load authorization and runtime planning result for KDNA
|
|
5
|
+
"description": "Machine-readable pre-load authorization and runtime planning result for packaged KDNA assets.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"kdna_version",
|
|
@@ -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) {
|