@aikdna/kdna-core 0.11.1 → 0.12.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/README.md +63 -154
- package/package.json +1 -1
- package/schema/load-plan.schema.json +119 -0
- package/src/types.d.ts +50 -0
- package/src/v1/index.js +325 -0
- package/src/v1/index.mjs +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @aikdna/kdna-core
|
|
2
2
|
|
|
3
|
-
Core library for
|
|
3
|
+
Core library for KDNA judgment assets.
|
|
4
|
+
|
|
5
|
+
KDNA Core v1 defines the official `.kdna` source/container contract used by
|
|
6
|
+
the CLI, Studio export, skills, MCP integrations, and downstream agent
|
|
7
|
+
runtimes.
|
|
4
8
|
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
@@ -8,187 +12,92 @@ Core library for loading, validating, linting, rendering, composing, and directl
|
|
|
8
12
|
npm install @aikdna/kdna-core
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Third-party adapters should start with the stable asset-first API. These
|
|
14
|
-
functions accept a `.kdna` file path, bytes, or an already opened asset and do
|
|
15
|
-
not require persistent extraction.
|
|
15
|
+
If you need full JSON Schema validation through Ajv, also install:
|
|
16
16
|
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
inspectKDNA,
|
|
20
|
-
validateKDNA,
|
|
21
|
-
loadKDNA,
|
|
22
|
-
renderForAgent,
|
|
23
|
-
verifyAsset,
|
|
24
|
-
verifyDigest,
|
|
25
|
-
verifySignature,
|
|
26
|
-
matchDomain,
|
|
27
|
-
composeKDNA
|
|
28
|
-
} = require('@aikdna/kdna-core');
|
|
29
|
-
|
|
30
|
-
const info = await inspectKDNA('./writing.kdna');
|
|
31
|
-
const validation = await validateKDNA('./writing.kdna');
|
|
32
|
-
const loaded = await loadKDNA('./writing.kdna', { profile: 'compact' });
|
|
33
|
-
const promptContext = await renderForAgent('./writing.kdna');
|
|
34
|
-
|
|
35
|
-
await verifyAsset('./writing.kdna', {
|
|
36
|
-
asset_digest: info.asset_digest,
|
|
37
|
-
requireSignature: true
|
|
38
|
-
});
|
|
39
|
-
await verifyDigest('./writing.kdna', info.asset_digest);
|
|
40
|
-
await verifySignature('./writing.kdna');
|
|
41
|
-
|
|
42
|
-
const matches = await matchDomain('Review this writing draft', ['./writing.kdna']);
|
|
43
|
-
const composed = await composeKDNA(['./writing.kdna', './agent_safety.kdna'], {
|
|
44
|
-
input: 'Review this public release note for safety and writing quality'
|
|
45
|
-
});
|
|
17
|
+
```bash
|
|
18
|
+
npm install ajv ajv-formats
|
|
46
19
|
```
|
|
47
20
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| Function | Purpose |
|
|
51
|
-
| --- | --- |
|
|
52
|
-
| `openKDNA()` | Open a `.kdna` file or bytes as an immutable asset. |
|
|
53
|
-
| `inspectKDNA()` | Return manifest, entries, access, quality, risk, and digests. |
|
|
54
|
-
| `validateKDNA()` | Run asset, lint, schema, and cross-file validation. |
|
|
55
|
-
| `loadKDNA()` | Load index/compact/scenario/full profiles directly from `.kdna`. |
|
|
56
|
-
| `renderForAgent()` | Render a loaded asset into agent prompt context. |
|
|
57
|
-
| `verifyAsset()` | Run full asset verification: digest, content digest, signature, and trust metadata. |
|
|
58
|
-
| `verifyDigest()` | Check whole-file `asset_digest`. |
|
|
59
|
-
| `verifySignature()` | Require Ed25519 signature verification. |
|
|
60
|
-
| `matchDomain()` | Rank candidate assets for a task string. |
|
|
61
|
-
| `composeKDNA()` | Compose multiple assets with attribution and conflict reporting. |
|
|
21
|
+
## KDNA Core v1 API
|
|
62
22
|
|
|
63
|
-
|
|
23
|
+
Use the `./v1` entrypoint for current KDNA Core v1 tooling:
|
|
64
24
|
|
|
65
25
|
```js
|
|
66
26
|
const {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const lintResult = lintDomain(dataMap);
|
|
80
|
-
const schemaResult = validateDomainSchema(dataMap, schemas);
|
|
81
|
-
const crossResult = validateCrossFile(dataMap);
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## API
|
|
85
|
-
|
|
86
|
-
### `createKdnaAssetReader()`
|
|
87
|
-
|
|
88
|
-
Direct `.kdna` container reader. The reader opens ZIP-backed `.kdna` assets without persistent extraction and exposes:
|
|
89
|
-
|
|
90
|
-
- `open(pathOrBytes)`
|
|
91
|
-
- `listEntries(asset)`
|
|
92
|
-
- `readEntry(asset, entryName)`
|
|
93
|
-
- `readJson(asset, entryName)`
|
|
94
|
-
- `readManifest(asset)`
|
|
95
|
-
- `readDataMap(asset)`
|
|
96
|
-
- `contentDigest(asset)`
|
|
97
|
-
- `verify(asset, { asset_digest?, content_digest?, requireSignature? })`
|
|
98
|
-
- `loadProfile(asset, "index" | "compact" | "scenario" | "full", options?)`
|
|
99
|
-
|
|
100
|
-
Example:
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
const { createKdnaAssetReader } = require('@aikdna/kdna-core');
|
|
104
|
-
|
|
105
|
-
const reader = createKdnaAssetReader();
|
|
106
|
-
const asset = await reader.open('./writing.kdna');
|
|
107
|
-
const manifest = await reader.readManifest(asset);
|
|
108
|
-
const trust = await reader.verify(asset, { requireSignature: true });
|
|
109
|
-
const loaded = await reader.loadProfile(asset, 'compact');
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
The asset reader treats extraction caches as implementation details. The `.kdna` file remains the identity, install, verification, and loading object.
|
|
113
|
-
|
|
114
|
-
Licensed assets can list encrypted JSON entries in `kdna.json`:
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
{
|
|
118
|
-
"access": "licensed",
|
|
119
|
-
"encryption": {
|
|
120
|
-
"profile": "kdna-licensed-entry-v1",
|
|
121
|
-
"encrypted_entries": ["KDNA_Core.json", "KDNA_Patterns.json"]
|
|
122
|
-
}
|
|
27
|
+
inspect,
|
|
28
|
+
validate,
|
|
29
|
+
pack,
|
|
30
|
+
unpack,
|
|
31
|
+
loadV1,
|
|
32
|
+
buildChecksumsV1
|
|
33
|
+
} = require('@aikdna/kdna-core/v1');
|
|
34
|
+
|
|
35
|
+
const validation = validate('./asset.kdna');
|
|
36
|
+
if (!validation.overall_valid) {
|
|
37
|
+
throw new Error(validation.problems.join('\n'));
|
|
123
38
|
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
The reader never writes decrypted entries to disk. Callers provide an in-memory
|
|
127
|
-
`decryptEntry` hook when they have already validated license activation:
|
|
128
|
-
|
|
129
|
-
```js
|
|
130
|
-
const { createLicensedDecryptEntry } = require('@aikdna/kdna-core');
|
|
131
39
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
40
|
+
const compact = loadV1('./asset.kdna', {
|
|
41
|
+
profile: 'compact',
|
|
42
|
+
as: 'prompt'
|
|
135
43
|
});
|
|
136
|
-
|
|
137
|
-
const loaded = await reader.loadProfile(asset, 'compact', { decryptEntry });
|
|
138
44
|
```
|
|
139
45
|
|
|
140
|
-
|
|
141
|
-
from the license key plus machine fingerprint using `scrypt-sha256`. This is a
|
|
142
|
-
runtime primitive, not a license activation system; callers must validate license
|
|
143
|
-
status before passing a decrypt hook to the reader.
|
|
144
|
-
|
|
145
|
-
### `lintDomain(dataMap)`
|
|
146
|
-
Structural linting — checks required files, field presence, unique IDs, yes/no answerable self-checks, cross-file references, and flags potentially vague axioms.
|
|
147
|
-
|
|
148
|
-
Returns `{ errors: string[], warnings: string[] }`.
|
|
149
|
-
|
|
150
|
-
### `validateDomainSchema(dataMap, schemaMap)`
|
|
151
|
-
JSON Schema validation against published schemas (KDNA_Core, KDNA_Patterns, KDNA_Scenarios, KDNA_Cases, KDNA_Reasoning, KDNA_Evolution).
|
|
46
|
+
## Supported Runtime Flow
|
|
152
47
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Renders domain files into a structured context block using a standard template. The rendered context preserves the domain's structure as distinct, named sections suitable for agent system prompts.
|
|
48
|
+
```text
|
|
49
|
+
v1 source directory
|
|
50
|
+
→ buildChecksumsV1
|
|
51
|
+
→ pack
|
|
52
|
+
→ validate
|
|
53
|
+
→ loadV1
|
|
54
|
+
→ agent/runtime context
|
|
55
|
+
```
|
|
162
56
|
|
|
163
|
-
##
|
|
57
|
+
## v1 Source Directory
|
|
164
58
|
|
|
165
|
-
|
|
59
|
+
A v1 source directory contains:
|
|
166
60
|
|
|
167
|
-
|
|
61
|
+
- `mimetype`
|
|
62
|
+
- `kdna.json`
|
|
63
|
+
- `payload.kdnab`
|
|
64
|
+
- `checksums.json`
|
|
168
65
|
|
|
169
|
-
|
|
66
|
+
## v1 Container
|
|
170
67
|
|
|
171
|
-
|
|
68
|
+
A v1 `.kdna` container is a zip package of the same files. `validate()` checks:
|
|
172
69
|
|
|
173
|
-
-
|
|
70
|
+
- format
|
|
71
|
+
- manifest schema
|
|
72
|
+
- payload parseability
|
|
73
|
+
- checksum consistency
|
|
74
|
+
- load-profile contract
|
|
174
75
|
|
|
175
|
-
|
|
76
|
+
## Load Profiles
|
|
176
77
|
|
|
177
|
-
|
|
78
|
+
`loadV1()` supports:
|
|
178
79
|
|
|
179
|
-
-
|
|
80
|
+
- `index`
|
|
81
|
+
- `compact`
|
|
82
|
+
- `scenario`
|
|
83
|
+
- `full`
|
|
180
84
|
|
|
181
|
-
|
|
85
|
+
Output formats:
|
|
182
86
|
|
|
183
|
-
-
|
|
87
|
+
- `json`
|
|
88
|
+
- `prompt`
|
|
184
89
|
|
|
185
|
-
|
|
90
|
+
## Boundary
|
|
186
91
|
|
|
187
|
-
|
|
92
|
+
KDNA Core v1 is content-neutral. It does not recommend assets, assign quality
|
|
93
|
+
badges, run a marketplace, or define a public registry. Future signature,
|
|
94
|
+
encryption, licensing, and entitlement work is gated outside the current v1
|
|
95
|
+
baseline.
|
|
188
96
|
|
|
189
|
-
|
|
97
|
+
## Legacy API
|
|
190
98
|
|
|
191
|
-
|
|
99
|
+
The package root still exports compatibility APIs for older KDNA paths. New
|
|
100
|
+
tooling should prefer `@aikdna/kdna-core/v1`.
|
|
192
101
|
|
|
193
102
|
## License
|
|
194
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "KDNA core library — load, validate, lint, and render KDNA domain judgment assets. Supports KDNA Container format (payload.kdnab via CBOR).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/aikdna/kdna/schema/load-plan.schema.json",
|
|
4
|
+
"title": "KDNA LoadPlan v1",
|
|
5
|
+
"description": "Machine-readable pre-load authorization and runtime planning result for KDNA Core v1 assets.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"kdna_version",
|
|
9
|
+
"asset",
|
|
10
|
+
"access",
|
|
11
|
+
"access_alias",
|
|
12
|
+
"entitlement_profile",
|
|
13
|
+
"state",
|
|
14
|
+
"required_action",
|
|
15
|
+
"can_load_now",
|
|
16
|
+
"projection_policy",
|
|
17
|
+
"checks",
|
|
18
|
+
"issues",
|
|
19
|
+
"source"
|
|
20
|
+
],
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"properties": {
|
|
23
|
+
"kdna_version": { "type": ["string", "null"] },
|
|
24
|
+
"asset": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"required": ["asset_id", "asset_uid", "title", "version", "judgment_version"],
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"properties": {
|
|
29
|
+
"asset_id": { "type": ["string", "null"] },
|
|
30
|
+
"asset_uid": { "type": ["string", "null"] },
|
|
31
|
+
"title": { "type": ["string", "null"] },
|
|
32
|
+
"version": { "type": ["string", "null"] },
|
|
33
|
+
"judgment_version": { "type": ["string", "null"] }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"access": {
|
|
37
|
+
"type": ["string", "null"],
|
|
38
|
+
"enum": ["public", "licensed", "remote", null]
|
|
39
|
+
},
|
|
40
|
+
"access_alias": { "type": ["string", "null"] },
|
|
41
|
+
"entitlement_profile": { "type": ["string", "null"] },
|
|
42
|
+
"state": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"enum": [
|
|
45
|
+
"ready",
|
|
46
|
+
"needs_password",
|
|
47
|
+
"needs_license",
|
|
48
|
+
"needs_account",
|
|
49
|
+
"needs_org_auth",
|
|
50
|
+
"needs_runtime",
|
|
51
|
+
"offline_grace",
|
|
52
|
+
"expired",
|
|
53
|
+
"revoked",
|
|
54
|
+
"invalid"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"required_action": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"enum": [
|
|
60
|
+
"none",
|
|
61
|
+
"load",
|
|
62
|
+
"enter_password",
|
|
63
|
+
"install_receipt",
|
|
64
|
+
"sign_in_or_activate",
|
|
65
|
+
"sync",
|
|
66
|
+
"connect_runtime",
|
|
67
|
+
"migrate_legacy",
|
|
68
|
+
"block"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"can_load_now": { "type": "boolean" },
|
|
72
|
+
"projection_policy": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"enum": ["minimal", "remote", "none"]
|
|
75
|
+
},
|
|
76
|
+
"checks": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"required": [
|
|
79
|
+
"format_valid",
|
|
80
|
+
"schema_valid",
|
|
81
|
+
"payload_valid",
|
|
82
|
+
"checksums_valid",
|
|
83
|
+
"load_contract_valid",
|
|
84
|
+
"overall_valid"
|
|
85
|
+
],
|
|
86
|
+
"additionalProperties": false,
|
|
87
|
+
"properties": {
|
|
88
|
+
"format_valid": { "type": "boolean" },
|
|
89
|
+
"schema_valid": { "type": "boolean" },
|
|
90
|
+
"payload_valid": { "type": "boolean" },
|
|
91
|
+
"checksums_valid": { "type": "boolean" },
|
|
92
|
+
"load_contract_valid": { "type": "boolean" },
|
|
93
|
+
"overall_valid": { "type": "boolean" }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"issues": {
|
|
97
|
+
"type": "array",
|
|
98
|
+
"items": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"required": ["code", "severity", "message"],
|
|
101
|
+
"additionalProperties": false,
|
|
102
|
+
"properties": {
|
|
103
|
+
"code": { "type": "string", "pattern": "^KDNA_[A-Z0-9_]+$" },
|
|
104
|
+
"severity": { "type": "string", "enum": ["info", "warning", "blocking"] },
|
|
105
|
+
"message": { "type": "string" }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"source": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"required": ["kind", "path"],
|
|
112
|
+
"additionalProperties": false,
|
|
113
|
+
"properties": {
|
|
114
|
+
"kind": { "type": ["string", "null"], "enum": ["dir", "file", null] },
|
|
115
|
+
"path": { "type": "string" }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/types.d.ts
CHANGED
|
@@ -507,6 +507,56 @@ export function isV1SourceDir(inputPath: string): boolean;
|
|
|
507
507
|
export function detectContainerFormat(inputPath: string): 'v1' | 'v2' | null;
|
|
508
508
|
export function inspect(inputPath: string, options?: Record<string, any>): Record<string, any>;
|
|
509
509
|
export function validate(inputPath: string, options?: Record<string, any>): Record<string, any>;
|
|
510
|
+
export interface KDNAV1LoadPlanIssue {
|
|
511
|
+
code: string;
|
|
512
|
+
severity: 'info' | 'warning' | 'blocking' | string;
|
|
513
|
+
message: string;
|
|
514
|
+
}
|
|
515
|
+
export interface KDNAV1LoadPlan {
|
|
516
|
+
kdna_version: string | null;
|
|
517
|
+
asset: {
|
|
518
|
+
asset_id: string | null;
|
|
519
|
+
asset_uid: string | null;
|
|
520
|
+
title: string | null;
|
|
521
|
+
version: string | null;
|
|
522
|
+
judgment_version: string | null;
|
|
523
|
+
};
|
|
524
|
+
access: 'public' | 'licensed' | 'remote' | string | null;
|
|
525
|
+
access_alias: string | null;
|
|
526
|
+
entitlement_profile: string | null;
|
|
527
|
+
state:
|
|
528
|
+
| 'ready'
|
|
529
|
+
| 'needs_password'
|
|
530
|
+
| 'needs_license'
|
|
531
|
+
| 'needs_account'
|
|
532
|
+
| 'needs_org_auth'
|
|
533
|
+
| 'needs_runtime'
|
|
534
|
+
| 'offline_grace'
|
|
535
|
+
| 'expired'
|
|
536
|
+
| 'revoked'
|
|
537
|
+
| 'invalid'
|
|
538
|
+
| string;
|
|
539
|
+
required_action:
|
|
540
|
+
| 'none'
|
|
541
|
+
| 'load'
|
|
542
|
+
| 'enter_password'
|
|
543
|
+
| 'install_receipt'
|
|
544
|
+
| 'sign_in_or_activate'
|
|
545
|
+
| 'sync'
|
|
546
|
+
| 'connect_runtime'
|
|
547
|
+
| 'migrate_legacy'
|
|
548
|
+
| 'block'
|
|
549
|
+
| string;
|
|
550
|
+
can_load_now: boolean;
|
|
551
|
+
projection_policy: 'minimal' | 'remote' | 'none' | string;
|
|
552
|
+
checks: Record<string, boolean>;
|
|
553
|
+
issues: KDNAV1LoadPlanIssue[];
|
|
554
|
+
source: {
|
|
555
|
+
kind: 'dir' | 'file' | string | null;
|
|
556
|
+
path: string;
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
export function planLoad(inputPath: string, options?: { password?: string; hasPassword?: boolean; entitlement?: Record<string, any> }): KDNAV1LoadPlan;
|
|
510
560
|
export function buildChecksumsV1(sourceDir: string): KDNAV1Checksums;
|
|
511
561
|
export function pack(sourceDir: string, outputPath: string): void;
|
|
512
562
|
export function unpack(inputPath: string, outputDir: string): void;
|
package/src/v1/index.js
CHANGED
|
@@ -750,6 +750,330 @@ function validate(inputPath, opts = {}) {
|
|
|
750
750
|
return runValidate(v1);
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
+
function normalizeAccess(access) {
|
|
754
|
+
const value = access || 'public';
|
|
755
|
+
if (value === 'open') return { access: 'public', alias: value };
|
|
756
|
+
if (value === 'protected') return { access: 'licensed', alias: value };
|
|
757
|
+
if (value === 'runtime') return { access: 'remote', alias: value };
|
|
758
|
+
return { access: value, alias: null };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function inferEntitlementProfile(manifest) {
|
|
762
|
+
if (manifest.entitlement && typeof manifest.entitlement.profile === 'string') {
|
|
763
|
+
return manifest.entitlement.profile;
|
|
764
|
+
}
|
|
765
|
+
if (manifest.encryption && manifest.encryption.profile === 'kdna-password-protected-v1') {
|
|
766
|
+
return 'password';
|
|
767
|
+
}
|
|
768
|
+
if (manifest.access === 'protected') return 'password';
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function buildLoadPlanIssue(code, severity, message) {
|
|
773
|
+
return { code, severity, message };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function validationProblemCode(problem) {
|
|
777
|
+
if (/checksums?:/i.test(problem)) return 'KDNA_INTEGRITY_DIGEST_FAILED';
|
|
778
|
+
if (/signature/i.test(problem)) return 'KDNA_INTEGRITY_SIGNATURE_FAILED';
|
|
779
|
+
if (/payload:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
|
|
780
|
+
if (/manifest:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
|
|
781
|
+
if (/load_contract:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
|
|
782
|
+
return 'KDNA_FORMAT_INVALID';
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function finalizeLoadPlan(plan) {
|
|
786
|
+
assertNoForbiddenTerms(plan);
|
|
787
|
+
return plan;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function baseLoadPlan(inputPath, v1, validation) {
|
|
791
|
+
const manifest = v1.manifest;
|
|
792
|
+
const accessInfo = normalizeAccess(manifest.access);
|
|
793
|
+
const entitlementProfile = inferEntitlementProfile(manifest);
|
|
794
|
+
const asset = {
|
|
795
|
+
asset_id: manifest.asset_id || null,
|
|
796
|
+
asset_uid: manifest.asset_uid || null,
|
|
797
|
+
title: manifest.title || null,
|
|
798
|
+
version: manifest.version || null,
|
|
799
|
+
judgment_version: manifest.judgment_version || null,
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
const plan = {
|
|
803
|
+
kdna_version: manifest.kdna_version || null,
|
|
804
|
+
asset,
|
|
805
|
+
access: accessInfo.access,
|
|
806
|
+
access_alias: accessInfo.alias,
|
|
807
|
+
entitlement_profile: entitlementProfile,
|
|
808
|
+
state: 'invalid',
|
|
809
|
+
required_action: 'block',
|
|
810
|
+
can_load_now: false,
|
|
811
|
+
projection_policy: 'none',
|
|
812
|
+
checks: {
|
|
813
|
+
format_valid: validation.format_valid,
|
|
814
|
+
schema_valid: validation.schema_valid,
|
|
815
|
+
payload_valid: validation.payload_valid,
|
|
816
|
+
checksums_valid: validation.checksums_valid,
|
|
817
|
+
load_contract_valid: validation.load_contract_valid,
|
|
818
|
+
overall_valid: validation.overall_valid,
|
|
819
|
+
},
|
|
820
|
+
issues: [],
|
|
821
|
+
source: {
|
|
822
|
+
kind: v1.kind,
|
|
823
|
+
path: path.resolve(inputPath),
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
if (accessInfo.alias) {
|
|
828
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
829
|
+
'KDNA_AUTH_ACCESS_ALIAS',
|
|
830
|
+
'info',
|
|
831
|
+
`Access value "${accessInfo.alias}" is treated as "${accessInfo.access}".`,
|
|
832
|
+
));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return plan;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Plan a KDNA v1 runtime load without decrypting or emitting judgment content.
|
|
840
|
+
* Product consumers such as Chat should render authorization UI from this
|
|
841
|
+
* result instead of parsing manifest fields directly.
|
|
842
|
+
*/
|
|
843
|
+
function planLoad(inputPath, opts = {}) {
|
|
844
|
+
let v1;
|
|
845
|
+
try {
|
|
846
|
+
v1 = readV1Layout(path.resolve(inputPath));
|
|
847
|
+
} catch (e) {
|
|
848
|
+
return finalizeLoadPlan({
|
|
849
|
+
kdna_version: null,
|
|
850
|
+
asset: {
|
|
851
|
+
asset_id: null,
|
|
852
|
+
asset_uid: null,
|
|
853
|
+
title: null,
|
|
854
|
+
version: null,
|
|
855
|
+
judgment_version: null,
|
|
856
|
+
},
|
|
857
|
+
access: null,
|
|
858
|
+
access_alias: null,
|
|
859
|
+
entitlement_profile: null,
|
|
860
|
+
state: 'invalid',
|
|
861
|
+
required_action: 'block',
|
|
862
|
+
can_load_now: false,
|
|
863
|
+
projection_policy: 'none',
|
|
864
|
+
checks: {
|
|
865
|
+
format_valid: false,
|
|
866
|
+
schema_valid: false,
|
|
867
|
+
payload_valid: false,
|
|
868
|
+
checksums_valid: false,
|
|
869
|
+
load_contract_valid: false,
|
|
870
|
+
overall_valid: false,
|
|
871
|
+
},
|
|
872
|
+
issues: [
|
|
873
|
+
buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message),
|
|
874
|
+
],
|
|
875
|
+
source: {
|
|
876
|
+
kind: null,
|
|
877
|
+
path: path.resolve(inputPath),
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const validation = runValidate(v1);
|
|
883
|
+
const plan = baseLoadPlan(inputPath, v1, validation);
|
|
884
|
+
|
|
885
|
+
if (!validation.overall_valid) {
|
|
886
|
+
plan.state = 'invalid';
|
|
887
|
+
plan.required_action = 'block';
|
|
888
|
+
plan.can_load_now = false;
|
|
889
|
+
plan.projection_policy = 'none';
|
|
890
|
+
for (const problem of validation.problems) {
|
|
891
|
+
plan.issues.push(buildLoadPlanIssue(validationProblemCode(problem), 'blocking', problem));
|
|
892
|
+
}
|
|
893
|
+
return finalizeLoadPlan(plan);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const manifest = v1.manifest;
|
|
897
|
+
const payloadDeclaredEncrypted =
|
|
898
|
+
manifest.payload && manifest.payload.encrypted === true;
|
|
899
|
+
const encryptedEntries = Array.isArray(manifest.encryption && manifest.encryption.encrypted_entries)
|
|
900
|
+
? manifest.encryption.encrypted_entries
|
|
901
|
+
: [];
|
|
902
|
+
const hasEncryptedPayload = payloadDeclaredEncrypted || encryptedEntries.length > 0;
|
|
903
|
+
|
|
904
|
+
if (!['public', 'licensed', 'remote'].includes(plan.access)) {
|
|
905
|
+
const unknownAccess = plan.access;
|
|
906
|
+
plan.access = null;
|
|
907
|
+
plan.state = 'invalid';
|
|
908
|
+
plan.required_action = 'block';
|
|
909
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
910
|
+
'KDNA_ACCESS_MODE_UNKNOWN',
|
|
911
|
+
'blocking',
|
|
912
|
+
`Unknown access value "${unknownAccess}".`,
|
|
913
|
+
));
|
|
914
|
+
return finalizeLoadPlan(plan);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (plan.access === 'remote') {
|
|
918
|
+
plan.state = 'needs_runtime';
|
|
919
|
+
plan.required_action = 'connect_runtime';
|
|
920
|
+
plan.can_load_now = false;
|
|
921
|
+
plan.projection_policy = 'remote';
|
|
922
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
923
|
+
'KDNA_AUTH_REMOTE_RUNTIME_REQUIRED',
|
|
924
|
+
'blocking',
|
|
925
|
+
'Remote assets require a runtime projection endpoint.',
|
|
926
|
+
));
|
|
927
|
+
return finalizeLoadPlan(plan);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (plan.access === 'licensed') {
|
|
931
|
+
const knownProfiles = new Set([
|
|
932
|
+
'password',
|
|
933
|
+
'local_receipt',
|
|
934
|
+
'account',
|
|
935
|
+
'org',
|
|
936
|
+
'purchase_receipt',
|
|
937
|
+
'device_bound',
|
|
938
|
+
]);
|
|
939
|
+
if (plan.entitlement_profile && !knownProfiles.has(plan.entitlement_profile)) {
|
|
940
|
+
plan.state = 'invalid';
|
|
941
|
+
plan.required_action = 'block';
|
|
942
|
+
plan.can_load_now = false;
|
|
943
|
+
plan.projection_policy = 'none';
|
|
944
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
945
|
+
'KDNA_ENTITLEMENT_PROFILE_UNKNOWN',
|
|
946
|
+
'blocking',
|
|
947
|
+
`Unknown entitlement profile "${plan.entitlement_profile}".`,
|
|
948
|
+
));
|
|
949
|
+
return finalizeLoadPlan(plan);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (plan.entitlement_profile === 'password') {
|
|
953
|
+
if (opts.password || opts.hasPassword === true) {
|
|
954
|
+
plan.state = 'ready';
|
|
955
|
+
plan.required_action = 'load';
|
|
956
|
+
plan.can_load_now = true;
|
|
957
|
+
plan.projection_policy = 'minimal';
|
|
958
|
+
} else {
|
|
959
|
+
plan.state = 'needs_password';
|
|
960
|
+
plan.required_action = 'enter_password';
|
|
961
|
+
plan.can_load_now = false;
|
|
962
|
+
plan.projection_policy = 'none';
|
|
963
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
964
|
+
'KDNA_AUTH_PASSWORD_REQUIRED',
|
|
965
|
+
'blocking',
|
|
966
|
+
'A password is required before this asset can be loaded.',
|
|
967
|
+
));
|
|
968
|
+
}
|
|
969
|
+
return finalizeLoadPlan(plan);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (plan.entitlement_profile === 'account') {
|
|
973
|
+
plan.state = 'needs_account';
|
|
974
|
+
plan.required_action = 'sign_in_or_activate';
|
|
975
|
+
plan.can_load_now = false;
|
|
976
|
+
plan.projection_policy = 'none';
|
|
977
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
978
|
+
'KDNA_AUTH_ACCOUNT_REQUIRED',
|
|
979
|
+
'blocking',
|
|
980
|
+
'Account authorization is required before this asset can be loaded.',
|
|
981
|
+
));
|
|
982
|
+
return finalizeLoadPlan(plan);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (plan.entitlement_profile === 'org') {
|
|
986
|
+
plan.state = 'needs_org_auth';
|
|
987
|
+
plan.required_action = 'sign_in_or_activate';
|
|
988
|
+
plan.can_load_now = false;
|
|
989
|
+
plan.projection_policy = 'none';
|
|
990
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
991
|
+
'KDNA_AUTH_ORG_REQUIRED',
|
|
992
|
+
'blocking',
|
|
993
|
+
'Organization authorization is required before this asset can be loaded.',
|
|
994
|
+
));
|
|
995
|
+
return finalizeLoadPlan(plan);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (opts.entitlement && opts.entitlement.status === 'active') {
|
|
999
|
+
plan.state = 'ready';
|
|
1000
|
+
plan.required_action = 'load';
|
|
1001
|
+
plan.can_load_now = true;
|
|
1002
|
+
plan.projection_policy = 'minimal';
|
|
1003
|
+
return finalizeLoadPlan(plan);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (opts.entitlement && opts.entitlement.status === 'expired') {
|
|
1007
|
+
plan.state = 'expired';
|
|
1008
|
+
plan.required_action = 'sync';
|
|
1009
|
+
plan.can_load_now = false;
|
|
1010
|
+
plan.projection_policy = 'none';
|
|
1011
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1012
|
+
'KDNA_AUTH_EXPIRED',
|
|
1013
|
+
'blocking',
|
|
1014
|
+
'The entitlement is expired.',
|
|
1015
|
+
));
|
|
1016
|
+
return finalizeLoadPlan(plan);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (opts.entitlement && opts.entitlement.status === 'revoked') {
|
|
1020
|
+
plan.state = 'revoked';
|
|
1021
|
+
plan.required_action = 'block';
|
|
1022
|
+
plan.can_load_now = false;
|
|
1023
|
+
plan.projection_policy = 'none';
|
|
1024
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1025
|
+
'KDNA_AUTH_REVOKED',
|
|
1026
|
+
'blocking',
|
|
1027
|
+
'The entitlement has been revoked.',
|
|
1028
|
+
));
|
|
1029
|
+
return finalizeLoadPlan(plan);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (opts.entitlement && opts.entitlement.status === 'offline_grace') {
|
|
1033
|
+
plan.state = 'offline_grace';
|
|
1034
|
+
plan.required_action = 'sync';
|
|
1035
|
+
plan.can_load_now = true;
|
|
1036
|
+
plan.projection_policy = 'minimal';
|
|
1037
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1038
|
+
'KDNA_AUTH_OFFLINE_GRACE_ACTIVE',
|
|
1039
|
+
'warning',
|
|
1040
|
+
'The entitlement can load during offline grace but must sync before grace expires.',
|
|
1041
|
+
));
|
|
1042
|
+
return finalizeLoadPlan(plan);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
plan.state = 'needs_license';
|
|
1046
|
+
plan.required_action = plan.entitlement_profile === 'local_receipt' ? 'install_receipt' : 'sign_in_or_activate';
|
|
1047
|
+
plan.can_load_now = false;
|
|
1048
|
+
plan.projection_policy = 'none';
|
|
1049
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1050
|
+
'KDNA_AUTH_ENTITLEMENT_REQUIRED',
|
|
1051
|
+
'blocking',
|
|
1052
|
+
'A valid entitlement is required before this asset can be loaded.',
|
|
1053
|
+
));
|
|
1054
|
+
return finalizeLoadPlan(plan);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (hasEncryptedPayload) {
|
|
1058
|
+
plan.state = 'invalid';
|
|
1059
|
+
plan.required_action = 'block';
|
|
1060
|
+
plan.can_load_now = false;
|
|
1061
|
+
plan.projection_policy = 'none';
|
|
1062
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1063
|
+
'KDNA_CRYPTO_PROFILE_UNSUPPORTED',
|
|
1064
|
+
'blocking',
|
|
1065
|
+
'Encrypted entries require licensed access.',
|
|
1066
|
+
));
|
|
1067
|
+
return finalizeLoadPlan(plan);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
plan.state = 'ready';
|
|
1071
|
+
plan.required_action = 'load';
|
|
1072
|
+
plan.can_load_now = true;
|
|
1073
|
+
plan.projection_policy = 'minimal';
|
|
1074
|
+
return finalizeLoadPlan(plan);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
753
1077
|
function assertNoForbiddenTerms(obj) {
|
|
754
1078
|
const seen = new Set();
|
|
755
1079
|
function walk(o) {
|
|
@@ -781,6 +1105,7 @@ module.exports = {
|
|
|
781
1105
|
readV1Layout,
|
|
782
1106
|
inspect,
|
|
783
1107
|
validate,
|
|
1108
|
+
planLoad,
|
|
784
1109
|
buildChecksumsV1,
|
|
785
1110
|
pack,
|
|
786
1111
|
unpack,
|
package/src/v1/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ export const detectContainerFormat = v1.detectContainerFormat;
|
|
|
9
9
|
export const readV1Layout = v1.readV1Layout;
|
|
10
10
|
export const inspect = v1.inspect;
|
|
11
11
|
export const validate = v1.validate;
|
|
12
|
+
export const planLoad = v1.planLoad;
|
|
12
13
|
export const buildChecksumsV1 = v1.buildChecksumsV1;
|
|
13
14
|
export const pack = v1.pack;
|
|
14
15
|
export const unpack = v1.unpack;
|