@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.
@@ -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 signatures/ are optional
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
- return require('../container-dispatcher.js');
57
- } catch {
58
- return { readAsset: null };
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', 'signatures', 'attachments'];
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 ((topLevel === 'signatures' || topLevel === 'attachments') && normalizedName === topLevel) {
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
- // subdirectory like signatures/ — record its presence but not contents here
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; signatures/ attachments/ etc.
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: opts.hasPassword === true,
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 (opts.password || opts.hasPassword === true) {
1679
- if (opts.hasPassword === true && !opts.password) {
1680
- plan.issues.push(buildLoadPlanIssue(
1681
- 'KDNA_AUTH_PASSWORD_DIAGNOSTIC',
1682
- 'info',
1683
- 'hasPassword is a diagnostic credential-presence signal only; it does not verify the password.',
1684
- ));
1685
- }
1686
- plan.state = 'ready';
1687
- plan.required_action = 'load';
1688
- plan.can_load_now = true;
1689
- plan.projection_policy = 'minimal';
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.slice(0, 2).join('; ')}`);
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.slice(0, 2).join('; ')}`);
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
- if (plan.can_load_now !== true) {
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')) ? axiom.one_sentence : (typeof axiom.full_statement === 'string' && axiom.full_statement.length > 0 ? axiom.full_statement.substring(0, 120) + (axiom.full_statement.length > 120 ? '…' : '') : statement);
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
- const decryptedBuf = typeof decrypted === 'string'
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
- const decryptedBuf = decryptFn(encryptedEnvelope, {
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).slice(0, 3),
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: pubkey
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,
@@ -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: iterations,
294
- m: memory_kib,
295
- p: parallelism,
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
- require(path.join(schemaRoot, 'external-grant-envelope.schema.json')),
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,
@@ -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
- const signatureEvidence = signature || { state: 'absent' };
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
  }