@aikdna/kdna-core 0.20.0 → 0.21.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 +52 -0
- package/README.md +10 -2
- package/package.json +1 -1
- package/schema/manifest.schema.json +24 -14
- package/schema/payload-profile.schema.json +156 -6
- package/schema/runtime-capsule.schema.json +45 -4
- package/src/asset-reader.js +9 -100
- package/src/container/index.js +275 -36
- package/src/crypto-profile.js +18 -3
- package/src/external-key-grant.js +4 -8
- package/src/index.mjs +0 -2
- package/src/public-api.js +0 -12
- package/src/runtime-capsule.js +19 -1
- package/src/types.d.ts +9 -19
package/src/container/index.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* - mimetype must be the first entry in a .kdna container
|
|
19
19
|
* - mimetype must be STORED (compression method 0) in a .kdna container
|
|
20
20
|
* - the source directory must contain mimetype, kdna.json, payload.kdnab
|
|
21
|
-
* - checksums.json and
|
|
21
|
+
* - checksums.json and attachments/ are optional
|
|
22
|
+
* - asset signatures are outside the current Preview contract
|
|
22
23
|
* - lineage must be a single object (not an array)
|
|
23
24
|
* - pack output is reproducible within one pinned toolchain/compressor;
|
|
24
25
|
* DEFLATE bytes may differ across compressors, zlib versions, or systems
|
|
@@ -51,17 +52,46 @@ const {
|
|
|
51
52
|
isVerifiedExternalEntitlement,
|
|
52
53
|
} = require('../external-key-grant');
|
|
53
54
|
|
|
55
|
+
function warnDispatcherDegraded(e) {
|
|
56
|
+
process.emitWarning(
|
|
57
|
+
`container-dispatcher failed to load; falling back to the legacy reader: ${e.message}`,
|
|
58
|
+
{ code: 'KDNA_DISPATCHER_DEGRADED' },
|
|
59
|
+
);
|
|
60
|
+
return { readAsset: null };
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
const { readAsset } = (() => {
|
|
64
|
+
const dispatcherRequest = '../container-dispatcher.js';
|
|
65
|
+
// Absence check via require.resolve: resolution only inspects the module
|
|
66
|
+
// path and never executes the target, so it can only fail with
|
|
67
|
+
// MODULE_NOT_FOUND when the dispatcher file itself cannot be located.
|
|
68
|
+
// Message matching is intentionally not used: Node prints the require
|
|
69
|
+
// stack inside MODULE_NOT_FOUND messages, so a dispatcher whose own
|
|
70
|
+
// internal dependency is missing would mention "container-dispatcher"
|
|
71
|
+
// and be misclassified as absent.
|
|
55
72
|
try {
|
|
56
|
-
|
|
57
|
-
} catch {
|
|
58
|
-
|
|
73
|
+
require.resolve(dispatcherRequest);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e && e.code === 'MODULE_NOT_FOUND') {
|
|
76
|
+
// The dispatcher file is genuinely absent: expected in minimal
|
|
77
|
+
// installs. Fall back silently.
|
|
78
|
+
return { readAsset: null };
|
|
79
|
+
}
|
|
80
|
+
return warnDispatcherDegraded(e);
|
|
81
|
+
}
|
|
82
|
+
// The file resolved; any failure from here on (missing internal
|
|
83
|
+
// dependency, syntax error, initialization throw, permission error) is a
|
|
84
|
+
// real load failure and must be surfaced.
|
|
85
|
+
try {
|
|
86
|
+
return require(dispatcherRequest);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return warnDispatcherDegraded(e);
|
|
59
89
|
}
|
|
60
90
|
})();
|
|
61
91
|
|
|
62
92
|
const MIMETYPE = 'application/vnd.kdna.asset';
|
|
63
93
|
const REQUIRED_DIR_ENTRIES = ['mimetype', 'kdna.json', 'payload.kdnab'];
|
|
64
|
-
const OPTIONAL_DIR_ENTRIES = ['checksums.json', '
|
|
94
|
+
const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'attachments'];
|
|
65
95
|
const ALLOWED_TOP_LEVEL_ENTRIES = new Set([
|
|
66
96
|
...REQUIRED_DIR_ENTRIES,
|
|
67
97
|
...OPTIONAL_DIR_ENTRIES,
|
|
@@ -405,7 +435,7 @@ function validateContainerEntryMetadata(entry) {
|
|
|
405
435
|
}
|
|
406
436
|
throw new Error(`container includes unsupported top-level entry: ${topLevel}`);
|
|
407
437
|
}
|
|
408
|
-
if (
|
|
438
|
+
if (topLevel === 'attachments' && normalizedName === topLevel) {
|
|
409
439
|
throw new Error(`container directory entry is not supported: ${name}`);
|
|
410
440
|
}
|
|
411
441
|
if (method !== 0 && method !== 8) {
|
|
@@ -558,7 +588,7 @@ function readLayout(absPath) {
|
|
|
558
588
|
if (fs.statSync(full).isFile()) {
|
|
559
589
|
map[f] = fs.readFileSync(full);
|
|
560
590
|
} else {
|
|
561
|
-
//
|
|
591
|
+
// Optional subdirectories are recorded without becoming payloads.
|
|
562
592
|
map[f] = null;
|
|
563
593
|
}
|
|
564
594
|
}
|
|
@@ -570,7 +600,7 @@ function readLayout(absPath) {
|
|
|
570
600
|
containerBytes = fs.readFileSync(absPath);
|
|
571
601
|
entries = listZipEntriesFromBuffer(containerBytes);
|
|
572
602
|
for (const e of entries) {
|
|
573
|
-
// We only need the well-known entries
|
|
603
|
+
// We only need the well-known entries and optional attachments.
|
|
574
604
|
// are passed through unchanged by the loader but not parsed here.
|
|
575
605
|
if (
|
|
576
606
|
e.name === 'mimetype' ||
|
|
@@ -732,6 +762,32 @@ function parsePayloadEntry(name, bytes, opts = {}) {
|
|
|
732
762
|
return parsed;
|
|
733
763
|
}
|
|
734
764
|
|
|
765
|
+
function assertDecryptedPayloadValid(payload, manifest) {
|
|
766
|
+
const validators = loadSchemas();
|
|
767
|
+
if (!validators) {
|
|
768
|
+
const error = new Error(
|
|
769
|
+
'Decrypted payload schema validation is unavailable.',
|
|
770
|
+
);
|
|
771
|
+
error.code = 'KDNA_PAYLOAD_SCHEMA_UNAVAILABLE';
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
const isBundleProfile =
|
|
775
|
+
manifest.compatibility?.profile === 'kdna.payload.bundle' ||
|
|
776
|
+
manifest.asset_type === 'bundle';
|
|
777
|
+
const validator = isBundleProfile
|
|
778
|
+
? validators.bundlePayload
|
|
779
|
+
: validators.payload;
|
|
780
|
+
if (validator(payload)) return payload;
|
|
781
|
+
const details = (validator.errors || [])
|
|
782
|
+
.map((entry) => `${entry.instancePath || '<root>'} ${entry.message}`)
|
|
783
|
+
.join('; ');
|
|
784
|
+
const error = new Error(
|
|
785
|
+
`Decrypted payload failed its declared schema${details ? `: ${details}` : '.'}`,
|
|
786
|
+
);
|
|
787
|
+
error.code = 'KDNA_PAYLOAD_INVALID';
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
|
|
735
791
|
// ─── inspect ───────────────────────────────────────────────────────────
|
|
736
792
|
|
|
737
793
|
/**
|
|
@@ -754,7 +810,6 @@ function buildInspectOutput(container) {
|
|
|
754
810
|
...assessLoaderCompatibility(m),
|
|
755
811
|
load_contract_default_profile: m.load_contract ? m.load_contract.default_profile : null,
|
|
756
812
|
};
|
|
757
|
-
if (m.signatures !== undefined) out.signature_count = Array.isArray(m.signatures) ? m.signatures.length : 0;
|
|
758
813
|
if (container.map['checksums.json']) out.checksums_present = true;
|
|
759
814
|
return out;
|
|
760
815
|
}
|
|
@@ -1322,7 +1377,9 @@ function inputFingerprint(inputPath, layout, opts = {}) {
|
|
|
1322
1377
|
) ? opts.entitlement.status : null;
|
|
1323
1378
|
|
|
1324
1379
|
return {
|
|
1325
|
-
has_password_input:
|
|
1380
|
+
has_password_input:
|
|
1381
|
+
opts.hasPassword === true ||
|
|
1382
|
+
(typeof opts.password === 'string' && opts.password.length > 0),
|
|
1326
1383
|
entitlement_input: entitlementInput,
|
|
1327
1384
|
source_fingerprint: computeSourceFingerprint(inputPath, layout),
|
|
1328
1385
|
};
|
|
@@ -1675,18 +1732,19 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1675
1732
|
}
|
|
1676
1733
|
|
|
1677
1734
|
if (plan.entitlement_profile === 'password') {
|
|
1678
|
-
if (
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
plan.
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1735
|
+
if (
|
|
1736
|
+
opts.hasPassword === true ||
|
|
1737
|
+
(typeof opts.password === 'string' && opts.password.length > 0)
|
|
1738
|
+
) {
|
|
1739
|
+
plan.state = 'needs_password';
|
|
1740
|
+
plan.required_action = 'enter_password';
|
|
1741
|
+
plan.can_load_now = false;
|
|
1742
|
+
plan.projection_policy = 'none';
|
|
1743
|
+
plan.issues.push(buildLoadPlanIssue(
|
|
1744
|
+
'KDNA_AUTH_PASSWORD_UNVERIFIED',
|
|
1745
|
+
'blocking',
|
|
1746
|
+
'A password was provided but has not been verified. Only an authorized load may verify it by decrypting the protected payload.',
|
|
1747
|
+
));
|
|
1690
1748
|
} else {
|
|
1691
1749
|
plan.state = 'needs_password';
|
|
1692
1750
|
plan.required_action = 'enter_password';
|
|
@@ -1958,10 +2016,10 @@ function renderPromptItem(item) {
|
|
|
1958
2016
|
if (item.type === 'axiom_applicability' && item.one_sentence) {
|
|
1959
2017
|
const parts = [item.one_sentence];
|
|
1960
2018
|
if (Array.isArray(item.applies_when) && item.applies_when.length) {
|
|
1961
|
-
parts.push(`applies when: ${item.applies_when.
|
|
2019
|
+
parts.push(`applies when: ${item.applies_when.join('; ')}`);
|
|
1962
2020
|
}
|
|
1963
2021
|
if (Array.isArray(item.does_not_apply_when) && item.does_not_apply_when.length) {
|
|
1964
|
-
parts.push(`does not apply when: ${item.does_not_apply_when.
|
|
2022
|
+
parts.push(`does not apply when: ${item.does_not_apply_when.join('; ')}`);
|
|
1965
2023
|
}
|
|
1966
2024
|
if (item.failure_risk) parts.push(`failure risk: ${item.failure_risk}`);
|
|
1967
2025
|
return parts.join(' — ');
|
|
@@ -2035,7 +2093,13 @@ function loadAuthorized(inputPath, opts = {}) {
|
|
|
2035
2093
|
const inputKind = typeof inputPath === 'string' ? 'packaged_file' : 'packaged_bytes';
|
|
2036
2094
|
const inputSnapshot = snapshotPackagedInput(inputPath);
|
|
2037
2095
|
const plan = planLoad(inputSnapshot, opts);
|
|
2038
|
-
|
|
2096
|
+
const loadMayVerifyPassword =
|
|
2097
|
+
typeof opts.password === 'string' &&
|
|
2098
|
+
opts.password.length > 0 &&
|
|
2099
|
+
plan.state === 'needs_password' &&
|
|
2100
|
+
Array.isArray(plan.issues) &&
|
|
2101
|
+
plan.issues.some((issue) => issue.code === 'KDNA_AUTH_PASSWORD_UNVERIFIED');
|
|
2102
|
+
if (plan.can_load_now !== true && !loadMayVerifyPassword) {
|
|
2039
2103
|
const issueCodes = Array.isArray(plan.issues)
|
|
2040
2104
|
? plan.issues.map((issue) => issue.code).filter(Boolean)
|
|
2041
2105
|
: [];
|
|
@@ -2226,7 +2290,11 @@ function normalizeCompactAxiom(axiom) {
|
|
|
2226
2290
|
if (!axiom || typeof axiom !== 'object') return null;
|
|
2227
2291
|
const statement = axiom.statement || axiom.one_sentence || axiom.full_statement || axiom.id || null;
|
|
2228
2292
|
if (!statement) return null;
|
|
2229
|
-
const oneSentence = (axiom.one_sentence && !String(axiom.one_sentence).startsWith('<TBD'))
|
|
2293
|
+
const oneSentence = (axiom.one_sentence && !String(axiom.one_sentence).startsWith('<TBD'))
|
|
2294
|
+
? axiom.one_sentence
|
|
2295
|
+
: (typeof axiom.full_statement === 'string' && axiom.full_statement.length > 0
|
|
2296
|
+
? axiom.full_statement
|
|
2297
|
+
: statement);
|
|
2230
2298
|
return {
|
|
2231
2299
|
type: 'axiom_applicability',
|
|
2232
2300
|
id: axiom.id || null,
|
|
@@ -2260,12 +2328,120 @@ function hasCompactPromptContent(content) {
|
|
|
2260
2328
|
hasStandardJudgmentRoleContent(content.judgment_role) ||
|
|
2261
2329
|
(Array.isArray(content.axioms) && content.axioms.length > 0) ||
|
|
2262
2330
|
(Array.isArray(content.boundaries) && content.boundaries.length > 0) ||
|
|
2331
|
+
(Array.isArray(content.core_structure) && content.core_structure.length > 0) ||
|
|
2263
2332
|
(Array.isArray(content.self_checks) && content.self_checks.length > 0) ||
|
|
2264
2333
|
(Array.isArray(content.failure_modes) && content.failure_modes.length > 0) ||
|
|
2265
2334
|
(Array.isArray(content.patterns) && content.patterns.length > 0)
|
|
2266
2335
|
);
|
|
2267
2336
|
}
|
|
2268
2337
|
|
|
2338
|
+
const COMPACT_PROJECTED_CORE_FIELDS = new Set([
|
|
2339
|
+
'highest_question',
|
|
2340
|
+
'worldview',
|
|
2341
|
+
'value_order',
|
|
2342
|
+
'judgment_role',
|
|
2343
|
+
'axioms',
|
|
2344
|
+
'boundaries',
|
|
2345
|
+
'core_structure',
|
|
2346
|
+
]);
|
|
2347
|
+
const COMPACT_PROJECTED_REASONING_FIELDS = new Set(['self_check', 'failure_modes']);
|
|
2348
|
+
const COMPACT_PROJECTED_AXIOM_FIELDS = new Set([
|
|
2349
|
+
'id',
|
|
2350
|
+
'statement',
|
|
2351
|
+
'one_sentence',
|
|
2352
|
+
'applies_when',
|
|
2353
|
+
'does_not_apply_when',
|
|
2354
|
+
'failure_risk',
|
|
2355
|
+
]);
|
|
2356
|
+
const COMPACT_NON_CONTENT_FIELDS = new Set(['profile', 'profile_version']);
|
|
2357
|
+
|
|
2358
|
+
function escapeJsonPointerToken(value) {
|
|
2359
|
+
return String(value).replaceAll('~', '~0').replaceAll('/', '~1');
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function omissionCount(value) {
|
|
2363
|
+
if (value === undefined || value === null) return 0;
|
|
2364
|
+
if (Array.isArray(value)) return value.length;
|
|
2365
|
+
if (typeof value === 'string') return value.trim() ? 1 : 0;
|
|
2366
|
+
if (typeof value === 'object') return Object.keys(value).length > 0 ? 1 : 0;
|
|
2367
|
+
return 1;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function addOmittedValue(entries, pointer, value) {
|
|
2371
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
2372
|
+
for (const key of Object.keys(value).sort()) {
|
|
2373
|
+
addOmittedValue(
|
|
2374
|
+
entries,
|
|
2375
|
+
`${pointer}/${escapeJsonPointerToken(key)}`,
|
|
2376
|
+
value[key],
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const count = omissionCount(value);
|
|
2382
|
+
if (count > 0) entries.push({ path: pointer, count });
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
function addOmittedAxiomFields(entries, axioms) {
|
|
2386
|
+
const counts = new Map();
|
|
2387
|
+
for (const axiom of Array.isArray(axioms) ? axioms : []) {
|
|
2388
|
+
if (!axiom || typeof axiom !== 'object' || Array.isArray(axiom)) continue;
|
|
2389
|
+
for (const key of Object.keys(axiom).sort()) {
|
|
2390
|
+
if (COMPACT_PROJECTED_AXIOM_FIELDS.has(key)) continue;
|
|
2391
|
+
const count = omissionCount(axiom[key]);
|
|
2392
|
+
if (count === 0) continue;
|
|
2393
|
+
const path = `/core/axioms/*/${escapeJsonPointerToken(key)}`;
|
|
2394
|
+
counts.set(path, (counts.get(path) || 0) + count);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
for (const path of [...counts.keys()].sort()) {
|
|
2398
|
+
entries.push({ path, count: counts.get(path) });
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function buildCompactProjectionReport(payload) {
|
|
2403
|
+
const entries = [];
|
|
2404
|
+
const core = payload && typeof payload.core === 'object' && !Array.isArray(payload.core)
|
|
2405
|
+
? payload.core
|
|
2406
|
+
: {};
|
|
2407
|
+
const reasoning = payload && typeof payload.reasoning === 'object' && !Array.isArray(payload.reasoning)
|
|
2408
|
+
? payload.reasoning
|
|
2409
|
+
: {};
|
|
2410
|
+
|
|
2411
|
+
for (const key of Object.keys(core).sort()) {
|
|
2412
|
+
if (!COMPACT_PROJECTED_CORE_FIELDS.has(key)) {
|
|
2413
|
+
addOmittedValue(entries, `/core/${escapeJsonPointerToken(key)}`, core[key]);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
addOmittedAxiomFields(entries, core.axioms);
|
|
2417
|
+
|
|
2418
|
+
for (const key of Object.keys(reasoning).sort()) {
|
|
2419
|
+
if (!COMPACT_PROJECTED_REASONING_FIELDS.has(key)) {
|
|
2420
|
+
addOmittedValue(entries, `/reasoning/${escapeJsonPointerToken(key)}`, reasoning[key]);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
for (const key of Object.keys(payload || {}).sort()) {
|
|
2425
|
+
if (
|
|
2426
|
+
COMPACT_NON_CONTENT_FIELDS.has(key) ||
|
|
2427
|
+
key === 'core' ||
|
|
2428
|
+
key === 'patterns' ||
|
|
2429
|
+
key === 'reasoning'
|
|
2430
|
+
) {
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
addOmittedValue(entries, `/${escapeJsonPointerToken(key)}`, payload[key]);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
2437
|
+
const omittedTotal = entries.reduce((total, entry) => total + entry.count, 0);
|
|
2438
|
+
return {
|
|
2439
|
+
status: omittedTotal > 0 ? 'partial' : 'complete',
|
|
2440
|
+
omitted: entries,
|
|
2441
|
+
omitted_total: omittedTotal,
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2269
2445
|
function loadAssetUnsafe(inputPath, opts = {}) {
|
|
2270
2446
|
const layout = readInputLayout(inputPath);
|
|
2271
2447
|
const m = layout.manifest;
|
|
@@ -2284,6 +2460,7 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2284
2460
|
// B4: Attempt decryption when password or decrypt hook is available.
|
|
2285
2461
|
if (opts.decryptEntry) {
|
|
2286
2462
|
// Consumer-provided decrypt hook (e.g., licensed entry key)
|
|
2463
|
+
let decryptedBuf;
|
|
2287
2464
|
try {
|
|
2288
2465
|
const decrypted = opts.decryptEntry({
|
|
2289
2466
|
asset: { entries: layout.map, manifest: m },
|
|
@@ -2291,18 +2468,27 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2291
2468
|
entryName: 'payload.kdnab',
|
|
2292
2469
|
ciphertext: layout.map['payload.kdnab'],
|
|
2293
2470
|
});
|
|
2294
|
-
|
|
2471
|
+
decryptedBuf = typeof decrypted === 'string'
|
|
2295
2472
|
? Buffer.from(decrypted, 'utf8')
|
|
2296
2473
|
: Buffer.from(decrypted);
|
|
2297
|
-
payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
|
|
2298
2474
|
} catch (e) {
|
|
2299
2475
|
const err = new Error(`Decryption failed: ${e.message}`);
|
|
2300
2476
|
err.code = e.code || 'KDNA_DECRYPT_FAILED';
|
|
2301
2477
|
throw err;
|
|
2302
2478
|
}
|
|
2479
|
+
try {
|
|
2480
|
+
payload = assertDecryptedPayloadValid(
|
|
2481
|
+
parsePayloadEntry('payload.kdnab', decryptedBuf),
|
|
2482
|
+
m,
|
|
2483
|
+
);
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
if (!error.code) error.code = 'KDNA_PAYLOAD_INVALID';
|
|
2486
|
+
throw error;
|
|
2487
|
+
}
|
|
2303
2488
|
} else if (opts.password) {
|
|
2304
2489
|
// Password-based decryption via kdna.encryption.password*
|
|
2305
2490
|
// profiles. Detects Argon2id vs scrypt from the envelope profile.
|
|
2491
|
+
let decryptedBuf;
|
|
2306
2492
|
try {
|
|
2307
2493
|
const {
|
|
2308
2494
|
decryptProtectedEntry,
|
|
@@ -2316,17 +2502,25 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2316
2502
|
const decryptFn = (envelope.profile === PASSWORD_PROTECTED_SCRYPT_PROFILE)
|
|
2317
2503
|
? decryptProtectedEntryScrypt
|
|
2318
2504
|
: decryptProtectedEntry;
|
|
2319
|
-
|
|
2505
|
+
decryptedBuf = decryptFn(encryptedEnvelope, {
|
|
2320
2506
|
entryName: 'payload.kdnab',
|
|
2321
2507
|
manifest: m,
|
|
2322
2508
|
password: opts.password,
|
|
2323
2509
|
});
|
|
2324
|
-
payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
|
|
2325
2510
|
} catch (e) {
|
|
2326
2511
|
const err = new Error(`Decryption failed with provided password: ${e.message}`);
|
|
2327
2512
|
err.code = 'KDNA_DECRYPT_FAILED';
|
|
2328
2513
|
throw err;
|
|
2329
2514
|
}
|
|
2515
|
+
try {
|
|
2516
|
+
payload = assertDecryptedPayloadValid(
|
|
2517
|
+
parsePayloadEntry('payload.kdnab', decryptedBuf),
|
|
2518
|
+
m,
|
|
2519
|
+
);
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
if (!error.code) error.code = 'KDNA_PAYLOAD_INVALID';
|
|
2522
|
+
throw error;
|
|
2523
|
+
}
|
|
2330
2524
|
} else {
|
|
2331
2525
|
const isExternal = payload.profile === EXTERNAL_ENVELOPE_PROFILE;
|
|
2332
2526
|
const err = new Error(
|
|
@@ -2408,6 +2602,32 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2408
2602
|
))
|
|
2409
2603
|
: []
|
|
2410
2604
|
);
|
|
2605
|
+
const projectCoreStructure = (items) => (
|
|
2606
|
+
Array.isArray(items)
|
|
2607
|
+
? items.map((relation) => {
|
|
2608
|
+
if (
|
|
2609
|
+
!relation ||
|
|
2610
|
+
typeof relation !== 'object' ||
|
|
2611
|
+
Array.isArray(relation)
|
|
2612
|
+
) {
|
|
2613
|
+
return null;
|
|
2614
|
+
}
|
|
2615
|
+
const projected = {};
|
|
2616
|
+
for (const key of [
|
|
2617
|
+
'from',
|
|
2618
|
+
'to',
|
|
2619
|
+
'via',
|
|
2620
|
+
'applies_when',
|
|
2621
|
+
'does_not_apply_when',
|
|
2622
|
+
]) {
|
|
2623
|
+
if (relation[key] !== undefined) {
|
|
2624
|
+
projected[key] = globalThis.structuredClone(relation[key]);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
return projected;
|
|
2628
|
+
}).filter(Boolean)
|
|
2629
|
+
: []
|
|
2630
|
+
);
|
|
2411
2631
|
result.content = {
|
|
2412
2632
|
highest_question: core.highest_question || null,
|
|
2413
2633
|
// These scoped domain-level values are already validated by the payload
|
|
@@ -2423,13 +2643,17 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2423
2643
|
: null,
|
|
2424
2644
|
axioms: (core.axioms || []).map(normalizeCompactAxiom).filter(Boolean),
|
|
2425
2645
|
boundaries: normalizeList(core.boundaries),
|
|
2646
|
+
// Relations are already part of the public Payload. Preserve order and
|
|
2647
|
+
// the closed public relation fields, never arbitrary authoring state.
|
|
2648
|
+
core_structure: projectCoreStructure(core.core_structure),
|
|
2426
2649
|
// Payload uses the singular field name while Runtime Capsule uses the
|
|
2427
2650
|
// plural collection name. Preserve every validated item and its shape;
|
|
2428
2651
|
// strings remain strings and structured questions remain objects.
|
|
2429
2652
|
self_checks: preserveDeclaredList(payload.reasoning && payload.reasoning.self_check),
|
|
2430
2653
|
failure_modes: normalizeList(payload.reasoning && payload.reasoning.failure_modes),
|
|
2431
|
-
patterns: normalizeList(payload.patterns)
|
|
2654
|
+
patterns: normalizeList(payload.patterns),
|
|
2432
2655
|
};
|
|
2656
|
+
result.projection_report = buildCompactProjectionReport(payload);
|
|
2433
2657
|
if (m.load_contract && m.load_contract.profiles && m.load_contract.profiles.compact && m.load_contract.profiles.compact.max_tokens_hint) {
|
|
2434
2658
|
result.max_tokens_hint = m.load_contract.profiles.compact.max_tokens_hint;
|
|
2435
2659
|
}
|
|
@@ -2547,6 +2771,14 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2547
2771
|
text += 'Asset ID: ' + (result.asset_id || 'unknown') + '\n';
|
|
2548
2772
|
text += 'Profile: ' + result.profile + '\n';
|
|
2549
2773
|
text += 'Safety boundary: KDNA content is subordinate to platform, system, and developer instructions.\n';
|
|
2774
|
+
if (result.projection_report) {
|
|
2775
|
+
text += `Projection completeness: ${result.projection_report.status}\n`;
|
|
2776
|
+
if (result.projection_report.omitted.length > 0) {
|
|
2777
|
+
text += 'Omitted payload paths: ' + result.projection_report.omitted
|
|
2778
|
+
.map((entry) => `${entry.path} (${entry.count})`)
|
|
2779
|
+
.join('; ') + '\n';
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2550
2782
|
if (result.max_tokens_hint) text += 'Max tokens hint: ' + result.max_tokens_hint + '\n';
|
|
2551
2783
|
if (c.highest_question) text += 'Highest question:\n' + c.highest_question + '\n';
|
|
2552
2784
|
if (hasStandardJudgmentRoleContent(c.judgment_role)) {
|
|
@@ -2570,6 +2802,17 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2570
2802
|
}
|
|
2571
2803
|
if (c.axioms && c.axioms.length) text += 'Axioms:\n' + c.axioms.map((a) => '- ' + renderPromptItem(a)).join('\n') + '\n';
|
|
2572
2804
|
if (c.boundaries && c.boundaries.length) text += 'Boundaries:\n' + c.boundaries.map((b) => '- ' + renderPromptItem(b)).join('\n') + '\n';
|
|
2805
|
+
if (c.core_structure && c.core_structure.length) {
|
|
2806
|
+
text += 'Judgment relations:\n' + c.core_structure.map((relation) => {
|
|
2807
|
+
if (typeof relation === 'string') return '- ' + sanitizePrompt(relation);
|
|
2808
|
+
if (!relation || typeof relation !== 'object' || Array.isArray(relation)) {
|
|
2809
|
+
return '- (unrendered relation)';
|
|
2810
|
+
}
|
|
2811
|
+
const from = sanitizePrompt(relation.from || 'unknown');
|
|
2812
|
+
const to = sanitizePrompt(relation.to || 'unknown');
|
|
2813
|
+
return `- ${from} --${sanitizePrompt(relation.via || 'relates_to')}--> ${to}`;
|
|
2814
|
+
}).join('\n') + '\n';
|
|
2815
|
+
}
|
|
2573
2816
|
if (c.self_checks && c.self_checks.length) text += 'Self-checks:\n' + c.self_checks.map((s) => '- ' + renderPromptItem(s)).join('\n') + '\n';
|
|
2574
2817
|
if (c.failure_modes && c.failure_modes.length) text += 'Failure modes:\n' + c.failure_modes.map((f) => '- ' + renderPromptItem(f)).join('\n') + '\n';
|
|
2575
2818
|
if (c.patterns && c.patterns.length) text += 'Patterns:\n' + c.patterns.map((p) => '- ' + renderPromptItem(p)).join('\n') + '\n';
|
|
@@ -2610,8 +2853,6 @@ function wrapAsCapsule(result, layout, profile, opts) {
|
|
|
2610
2853
|
function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
|
|
2611
2854
|
const m = layout.manifest;
|
|
2612
2855
|
const val = opts._validation || {};
|
|
2613
|
-
const pubkey = (m.creator && m.creator.pubkey) || null;
|
|
2614
|
-
const sigVerified = val.signature_valid === true;
|
|
2615
2856
|
const runtimeCapsule = require('../runtime-capsule');
|
|
2616
2857
|
const digests = runtimeCapsule.computeDigestEvidence(opts._runtimeAssetBytes, {
|
|
2617
2858
|
expectedDigests: opts.expectedDigests,
|
|
@@ -2620,9 +2861,7 @@ function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
|
|
|
2620
2861
|
projection: loadResult,
|
|
2621
2862
|
manifest: m,
|
|
2622
2863
|
digests,
|
|
2623
|
-
signature:
|
|
2624
|
-
? { state: sigVerified ? 'verified' : 'not_checked', issuer: pubkey }
|
|
2625
|
-
: { state: 'absent' },
|
|
2864
|
+
signature: { state: 'absent' },
|
|
2626
2865
|
inputKind: opts._runtimeInputKind,
|
|
2627
2866
|
loadedAt: opts.loadedAt,
|
|
2628
2867
|
schemaValid: val.schema_valid === true,
|
package/src/crypto-profile.js
CHANGED
|
@@ -278,6 +278,18 @@ function ensureArgon2id() {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
const MAX_ARGON2_MEMORY_KIB = 262144; // 256 MiB
|
|
282
|
+
const MAX_ARGON2_ITERATIONS = 16;
|
|
283
|
+
const MAX_ARGON2_PARALLELISM = 8;
|
|
284
|
+
|
|
285
|
+
function boundedArgon2Param(value, name, max) {
|
|
286
|
+
if (value === undefined) return undefined;
|
|
287
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max) {
|
|
288
|
+
throw new Error(`unsupported Argon2id ${name}: must be an integer between 1 and ${max}`);
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
function derivePasswordKey(password, params = {}) {
|
|
282
294
|
ensureArgon2id();
|
|
283
295
|
const {
|
|
@@ -287,12 +299,15 @@ function derivePasswordKey(password, params = {}) {
|
|
|
287
299
|
parallelism = 4,
|
|
288
300
|
} = params;
|
|
289
301
|
if (!salt) throw new Error('salt is required for Argon2id');
|
|
302
|
+
const boundedMemory = boundedArgon2Param(memory_kib, 'memory_kib', MAX_ARGON2_MEMORY_KIB);
|
|
303
|
+
const boundedIterations = boundedArgon2Param(iterations, 'iterations', MAX_ARGON2_ITERATIONS);
|
|
304
|
+
const boundedParallelism = boundedArgon2Param(parallelism, 'parallelism', MAX_ARGON2_PARALLELISM);
|
|
290
305
|
const saltBuf = decodeBase64(salt, 'salt');
|
|
291
306
|
const passwordBuf = toBuffer(password, 'password');
|
|
292
307
|
const key = argon2id(passwordBuf, saltBuf, {
|
|
293
|
-
t:
|
|
294
|
-
m:
|
|
295
|
-
p:
|
|
308
|
+
t: boundedIterations,
|
|
309
|
+
m: boundedMemory,
|
|
310
|
+
p: boundedParallelism,
|
|
296
311
|
dkLen: 32,
|
|
297
312
|
});
|
|
298
313
|
return Buffer.from(key);
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const crypto = require('node:crypto');
|
|
10
|
-
const path = require('node:path');
|
|
11
10
|
const JsonSchema2020 = require('ajv/dist/2020');
|
|
12
11
|
const addFormats = require('ajv-formats');
|
|
13
12
|
const cbor = require('cbor-x');
|
|
14
13
|
const { hkdfSha256, aesWrap, aesUnwrap } = require('./crypto-profile');
|
|
14
|
+
const externalEnvelopeSchema = require('../schema/external-grant-envelope.schema.json');
|
|
15
|
+
const externalKeyGrantSchema = require('../schema/external-key-grant.schema.json');
|
|
15
16
|
|
|
16
17
|
const EXTERNAL_ENVELOPE_PROFILE = 'kdna.envelope.external-grant';
|
|
17
18
|
const EXTERNAL_GRANT_PROFILE = 'kdna.grant.external-key';
|
|
@@ -21,15 +22,10 @@ const EXTERNAL_GRANT_CONTRACT_VERSION = '0.1.0';
|
|
|
21
22
|
const ENVELOPE_ALG = 'A256GCM';
|
|
22
23
|
const GRANT_WRAP_ALG = 'X25519-HKDF-SHA256+A256KW';
|
|
23
24
|
|
|
24
|
-
const schemaRoot = path.join(__dirname, '..', 'schema');
|
|
25
25
|
const ajv = new JsonSchema2020({ allErrors: true, strict: true });
|
|
26
26
|
addFormats(ajv);
|
|
27
|
-
const validateEnvelopeSchema = ajv.compile(
|
|
28
|
-
|
|
29
|
-
);
|
|
30
|
-
const validateGrantSchema = ajv.compile(
|
|
31
|
-
require(path.join(schemaRoot, 'external-key-grant.schema.json')),
|
|
32
|
-
);
|
|
27
|
+
const validateEnvelopeSchema = ajv.compile(externalEnvelopeSchema);
|
|
28
|
+
const validateGrantSchema = ajv.compile(externalKeyGrantSchema);
|
|
33
29
|
|
|
34
30
|
const verifiedEntitlements = new WeakSet();
|
|
35
31
|
|
package/src/index.mjs
CHANGED
|
@@ -78,8 +78,6 @@ export const verifyAsset = publicApi.verifyAsset;
|
|
|
78
78
|
export const verifyAssetSync = publicApi.verifyAssetSync;
|
|
79
79
|
export const verifyDigest = publicApi.verifyDigest;
|
|
80
80
|
export const verifyDigestSync = publicApi.verifyDigestSync;
|
|
81
|
-
export const verifySignature = publicApi.verifySignature;
|
|
82
|
-
export const verifySignatureSync = publicApi.verifySignatureSync;
|
|
83
81
|
export const matchDomain = publicApi.matchDomain;
|
|
84
82
|
export const matchDomainSync = publicApi.matchDomainSync;
|
|
85
83
|
export const composeKDNA = publicApi.composeKDNA;
|
package/src/public-api.js
CHANGED
|
@@ -63,7 +63,6 @@ async function inspectKDNA(input, options = {}) {
|
|
|
63
63
|
entries,
|
|
64
64
|
asset_digest: asset.asset_digest,
|
|
65
65
|
content_digest: contentDigest,
|
|
66
|
-
signature_valid: verification ? verification.signature_valid : null,
|
|
67
66
|
ok: verification ? verification.ok : null,
|
|
68
67
|
errors: verification ? verification.errors : [],
|
|
69
68
|
warnings: verification ? verification.warnings : [],
|
|
@@ -84,7 +83,6 @@ function inspectKDNASync(input, options = {}) {
|
|
|
84
83
|
entries,
|
|
85
84
|
asset_digest: asset.asset_digest,
|
|
86
85
|
content_digest: contentDigest,
|
|
87
|
-
signature_valid: verification ? verification.signature_valid : null,
|
|
88
86
|
ok: verification ? verification.ok : null,
|
|
89
87
|
errors: verification ? verification.errors : [],
|
|
90
88
|
warnings: verification ? verification.warnings : [],
|
|
@@ -138,14 +136,6 @@ function verifyDigestSync(input, expectedDigest, options = {}) {
|
|
|
138
136
|
return verifyAssetSync(input, { ...options, asset_digest: expectedDigest });
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
async function verifySignature(input, options = {}) {
|
|
142
|
-
return verifyAsset(input, { ...options, requireSignature: true });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function verifySignatureSync(input, options = {}) {
|
|
146
|
-
return verifyAssetSync(input, { ...options, requireSignature: true });
|
|
147
|
-
}
|
|
148
|
-
|
|
149
139
|
function scoreMatch(input, info) {
|
|
150
140
|
const haystack = String(input || '').toLowerCase();
|
|
151
141
|
const terms = [
|
|
@@ -211,8 +201,6 @@ module.exports = {
|
|
|
211
201
|
verifyAssetSync,
|
|
212
202
|
verifyDigest,
|
|
213
203
|
verifyDigestSync,
|
|
214
|
-
verifySignature,
|
|
215
|
-
verifySignatureSync,
|
|
216
204
|
matchDomain,
|
|
217
205
|
matchDomainSync,
|
|
218
206
|
composeKDNA,
|
package/src/runtime-capsule.js
CHANGED
|
@@ -475,7 +475,22 @@ function buildRuntimeCapsule({
|
|
|
475
475
|
if (!['public', 'licensed', 'remote'].includes(runtimeAccess)) {
|
|
476
476
|
fail('KDNA_RUNTIME_CAPSULE_BUILD_INVALID', `Unsupported Runtime access: ${runtimeAccess}.`);
|
|
477
477
|
}
|
|
478
|
-
|
|
478
|
+
if (
|
|
479
|
+
signature !== undefined &&
|
|
480
|
+
(
|
|
481
|
+
!signature ||
|
|
482
|
+
typeof signature !== 'object' ||
|
|
483
|
+
Array.isArray(signature) ||
|
|
484
|
+
signature.state !== 'absent' ||
|
|
485
|
+
Object.keys(signature).some((key) => key !== 'state')
|
|
486
|
+
)
|
|
487
|
+
) {
|
|
488
|
+
fail(
|
|
489
|
+
'KDNA_ASSET_SIGNATURE_UNSUPPORTED',
|
|
490
|
+
'Asset signatures are outside the current Preview contract.',
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const signatureEvidence = { state: 'absent' };
|
|
479
494
|
|
|
480
495
|
const capsule = {
|
|
481
496
|
type: 'kdna.runtime-capsule',
|
|
@@ -502,6 +517,9 @@ function buildRuntimeCapsule({
|
|
|
502
517
|
profile: projection.profile,
|
|
503
518
|
},
|
|
504
519
|
};
|
|
520
|
+
if (projection.projection_report !== undefined) {
|
|
521
|
+
capsule.trace.projection_report = globalThis.structuredClone(projection.projection_report);
|
|
522
|
+
}
|
|
505
523
|
assertRuntimeCapsuleSuccess(capsule, 'KDNA_RUNTIME_CAPSULE_BUILD_INVALID');
|
|
506
524
|
return capsule;
|
|
507
525
|
}
|