@aikdna/kdna-core 0.17.0 → 0.19.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +227 -64
  2. package/README.md +114 -77
  3. package/package.json +8 -2
  4. package/schema/agent-host-capabilities.schema.json +62 -0
  5. package/schema/agent-host-receipt.schema.json +371 -0
  6. package/schema/agent-host-request.schema.json +223 -0
  7. package/schema/{bundle-profile-v1.schema.json → bundle-profile.schema.json} +9 -5
  8. package/schema/checksums.schema.json +42 -3
  9. package/schema/consumption-plan.schema.json +428 -0
  10. package/schema/digest-evidence.schema.json +212 -0
  11. package/schema/{kdna-envelope-external-grant-v1.schema.json → external-grant-envelope.schema.json} +17 -4
  12. package/schema/{kdna-key-grant-v1.schema.json → external-key-grant.schema.json} +39 -7
  13. package/schema/judgment-trace.schema.json +1251 -0
  14. package/schema/load-plan.schema.json +7 -5
  15. package/schema/manifest.schema.json +70 -10
  16. package/schema/{payload-profile-v1.schema.json → payload-profile.schema.json} +30 -6
  17. package/schema/runtime-capsule.schema.json +116 -0
  18. package/src/asset-reader.js +106 -120
  19. package/src/{v1 → container}/index.js +566 -209
  20. package/src/container/index.mjs +20 -0
  21. package/src/container-dispatcher.js +10 -7
  22. package/src/crypto-profile.js +37 -107
  23. package/src/external-key-grant.js +41 -15
  24. package/src/index.js +11 -4
  25. package/src/index.mjs +143 -24
  26. package/src/loader-compatibility.js +59 -0
  27. package/src/loader.js +1 -1
  28. package/src/public-api.js +59 -163
  29. package/src/remote-runtime.d.ts +13 -0
  30. package/src/remote-runtime.js +27 -0
  31. package/src/remote-runtime.mjs +3 -0
  32. package/src/runtime-api.js +9 -10
  33. package/src/runtime-capsule.js +529 -0
  34. package/src/runtime-contract.js +1882 -0
  35. package/src/types.d.ts +1127 -136
  36. package/src/workpack-pure.js +1 -1
  37. package/src/lint-pure.js +0 -430
  38. package/src/v1/index.mjs +0 -20
@@ -0,0 +1,20 @@
1
+ import containerApi from './index.js';
2
+
3
+ export const MIMETYPE = containerApi.MIMETYPE;
4
+ export const REQUIRED_DIR_ENTRIES = containerApi.REQUIRED_DIR_ENTRIES;
5
+ export const isKdnaSourceDir = containerApi.isKdnaSourceDir;
6
+ export const detectContainerFormat = containerApi.detectContainerFormat;
7
+ export const readLayout = containerApi.readLayout;
8
+ export const inspect = containerApi.inspect;
9
+ export const validate = containerApi.validate;
10
+ export const planLoad = containerApi.planLoad;
11
+ export const loadAuthorized = containerApi.loadAuthorized;
12
+ export const buildChecksums = containerApi.buildChecksums;
13
+ export const computeRuntimeEntrySetDigest = containerApi.computeRuntimeEntrySetDigest;
14
+ export const pack = containerApi.pack;
15
+ export const unpack = containerApi.unpack;
16
+ export const load = containerApi.load;
17
+ export const loadAsset = containerApi.loadAsset;
18
+ export const FORBIDDEN_OUTPUT_TERMS = containerApi.FORBIDDEN_OUTPUT_TERMS;
19
+
20
+ export default containerApi;
@@ -18,7 +18,8 @@ const MIMETYPE = 'application/vnd.kdna.asset';
18
18
  * @property {object} manifest — parsed kdna.json
19
19
  * @property {Buffer} payloadRaw — raw payload.kdnab bytes
20
20
  * @property {object|null} checksums
21
- * @property {Buffer|null} assetDigest
21
+ * @property {Buffer|null} assetDigest — legacy internal name for final container bytes
22
+ * @property {string|null} containerDigest — tagged SHA-256 of final container bytes
22
23
  * @property {Buffer|null} containerBuffer
23
24
  * @property {Map<string, Buffer>|null} entries
24
25
  */
@@ -177,13 +178,14 @@ function readSourceDirectory(absPath, _opts) {
177
178
  payloadRaw,
178
179
  checksums,
179
180
  assetDigest: null,
181
+ containerDigest: null,
180
182
  containerBuffer: null,
181
183
  entries,
182
184
  };
183
185
  }
184
186
 
185
187
  function readKdnaContainer(absPath, _opts) {
186
- const v1Layout = require('./v1/index.js').readLayout(absPath);
188
+ const layout = require('./container/index.js').readLayout(absPath);
187
189
  const containerBuf = fs.readFileSync(absPath);
188
190
  const assetDigest = crypto.createHash('sha256').update(containerBuf).digest();
189
191
 
@@ -192,14 +194,15 @@ function readKdnaContainer(absPath, _opts) {
192
194
  sourcePath: absPath,
193
195
  format: 'kdna',
194
196
  mimetype: MIMETYPE,
195
- manifest: v1Layout.manifest,
196
- payloadRaw: v1Layout.map['payload.kdnab'] || null,
197
- checksums: v1Layout.map['checksums.json']
198
- ? (() => { try { return JSON.parse(v1Layout.map['checksums.json'].toString('utf8')); } catch { return null; } })()
197
+ manifest: layout.manifest,
198
+ payloadRaw: layout.map['payload.kdnab'] || null,
199
+ checksums: layout.map['checksums.json']
200
+ ? (() => { try { return JSON.parse(layout.map['checksums.json'].toString('utf8')); } catch { return null; } })()
199
201
  : null,
200
202
  assetDigest,
203
+ containerDigest: `sha256:${assetDigest.toString('hex')}`,
201
204
  containerBuffer: containerBuf,
202
- entries: new Map(Object.entries(v1Layout.map)),
205
+ entries: new Map(Object.entries(layout.map)),
203
206
  };
204
207
  }
205
208
 
@@ -17,14 +17,7 @@ try {
17
17
  * - AES-256-GCM content encryption
18
18
  * - Random CEK per asset; license key only unwraps CEK, never touches content
19
19
  */
20
- const LICENSED_ENTRY_PROFILE = 'kdna-licensed-entry-v1';
21
-
22
- /**
23
- * Pre-RFC legacy profile (now experimental).
24
- * Uses scrypt-sha256 with concatenated secret (`licenseKey|machineFingerprint`).
25
- * Retained for backward compatibility only.
26
- */
27
- const LICENSED_EXPERIMENTAL_PROFILE = 'kdna-licensed-entry-experimental';
20
+ const LICENSED_ENTRY_PROFILE = 'kdna.encryption.licensed-entry';
28
21
 
29
22
  /**
30
23
  * RFC-0009 compliant profile.
@@ -33,11 +26,11 @@ const LICENSED_EXPERIMENTAL_PROFILE = 'kdna-licensed-entry-experimental';
33
26
  * - AES-256-GCM content encryption
34
27
  * - Dual key slots: password + recovery
35
28
  */
36
- const PASSWORD_PROTECTED_PROFILE = 'kdna-password-protected-v1';
29
+ const PASSWORD_PROTECTED_PROFILE = 'kdna.encryption.password';
30
+ const ENCRYPTION_PROFILE_VERSION = '0.1.0';
37
31
 
38
32
  const RFC_KDF = 'HKDF-SHA256';
39
33
  const RFC_KEY_WRAPPING = 'AES-256-KW';
40
- const LEGACY_KDF = 'scrypt-sha256';
41
34
  const PASSWORD_KDF = 'Argon2id';
42
35
  const ALG = 'AES-256-GCM';
43
36
 
@@ -63,14 +56,20 @@ function normalizeEnvelope(value) {
63
56
  try { return cbor.decode(Buffer.from(value, 'utf8')); } catch { /* fall through */ }
64
57
  }
65
58
  if (value && typeof value === 'object') return value;
66
- throw new Error('encrypted entry envelope must be CBOR (kdna_version 1.0)');
59
+ throw new Error('encrypted entry envelope must be CBOR (format 0.1.0)');
67
60
  }
68
61
 
69
- function encryptedEntryAad(entryName, manifest = {}, profile = LICENSED_ENTRY_PROFILE) {
62
+ function encryptedEntryAad(
63
+ entryName,
64
+ manifest = {},
65
+ profile = LICENSED_ENTRY_PROFILE,
66
+ profileVersion = ENCRYPTION_PROFILE_VERSION,
67
+ ) {
70
68
  return Buffer.from(
71
69
  [
72
70
  profile,
73
- manifest.name || manifest.asset_id || '',
71
+ profileVersion,
72
+ manifest.asset_id || '',
74
73
  manifest.version || '',
75
74
  entryName,
76
75
  ].join('\n'),
@@ -173,7 +172,7 @@ function aesUnwrap(key, ciphertext) {
173
172
  /**
174
173
  * Derive a key-wrapping key (KWK) from a license key via HKDF-SHA256.
175
174
  */
176
- function deriveWrappingKey(licenseKey, info = 'kdna-licensed-entry-v1-kwk') {
175
+ function deriveWrappingKey(licenseKey, info = 'kdna.encryption.licensed-entry-kwk') {
177
176
  return hkdfSha256(licenseKey, null, info, 32);
178
177
  }
179
178
 
@@ -207,7 +206,7 @@ function unwrapCEK(wrappedCek, wrappingKey) {
207
206
  * 4. Wrap CEK with KWK (AES-256-KW)
208
207
  * 5. Store wrapped_key in envelope
209
208
  */
210
- function encryptLicensedEntryV1(plaintext, options = {}) {
209
+ function encryptLicensedEntry(plaintext, options = {}) {
211
210
  const { entryName, manifest = {}, licenseKey } = options;
212
211
  if (!entryName) throw new Error('entryName is required');
213
212
  if (!licenseKey) throw new Error('licenseKey is required for RFC-0008 encryption');
@@ -223,6 +222,7 @@ function encryptLicensedEntryV1(plaintext, options = {}) {
223
222
 
224
223
  return {
225
224
  profile: LICENSED_ENTRY_PROFILE,
225
+ profile_version: ENCRYPTION_PROFILE_VERSION,
226
226
  alg: ALG,
227
227
  kdf: RFC_KDF,
228
228
  key_wrapping: RFC_KEY_WRAPPING,
@@ -240,7 +240,7 @@ function encryptLicensedEntryV1(plaintext, options = {}) {
240
240
  * 2. Unwrap CEK from wrapped_key (AES-256-KW)
241
241
  * 3. Decrypt content with CEK (AES-256-GCM)
242
242
  */
243
- function decryptLicensedEntryV1(envelopeValue, options = {}) {
243
+ function decryptLicensedEntry(envelopeValue, options = {}) {
244
244
  const { entryName, manifest = {}, licenseKey } = options;
245
245
  if (!entryName) throw new Error('entryName is required');
246
246
  if (!licenseKey) throw new Error('licenseKey is required for RFC-0008 decryption');
@@ -249,6 +249,9 @@ function decryptLicensedEntryV1(envelopeValue, options = {}) {
249
249
  if (envelope.profile !== LICENSED_ENTRY_PROFILE) {
250
250
  throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'} (expected ${LICENSED_ENTRY_PROFILE})`);
251
251
  }
252
+ if (envelope.profile_version !== ENCRYPTION_PROFILE_VERSION) {
253
+ throw new Error(`unsupported encrypted entry profile_version: ${envelope.profile_version || 'unknown'}`);
254
+ }
252
255
  if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
253
256
  if (envelope.kdf !== RFC_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
254
257
  if (envelope.key_wrapping !== RFC_KEY_WRAPPING) throw new Error(`unsupported encrypted entry key_wrapping: ${envelope.key_wrapping}`);
@@ -358,6 +361,7 @@ function encryptProtectedEntry(plaintext, options = {}) {
358
361
 
359
362
  return {
360
363
  profile: PASSWORD_PROTECTED_PROFILE,
364
+ profile_version: ENCRYPTION_PROFILE_VERSION,
361
365
  alg: ALG,
362
366
  kdf: PASSWORD_KDF,
363
367
  key_wrapping: RFC_KEY_WRAPPING,
@@ -382,6 +386,9 @@ function decryptProtectedEntry(envelopeValue, options = {}) {
382
386
  `unsupported encrypted entry profile: ${envelope.profile || 'unknown'} (expected ${PASSWORD_PROTECTED_PROFILE})`,
383
387
  );
384
388
  }
389
+ if (envelope.profile_version !== ENCRYPTION_PROFILE_VERSION) {
390
+ throw new Error(`unsupported encrypted entry profile_version: ${envelope.profile_version || 'unknown'}`);
391
+ }
385
392
  if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
386
393
  if (envelope.kdf !== PASSWORD_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
387
394
  if (envelope.key_wrapping !== RFC_KEY_WRAPPING) {
@@ -422,12 +429,12 @@ function createRecoveryDecryptEntry(options = {}) {
422
429
  decryptProtectedEntry(ciphertext, { entryName, manifest, recoveryCode });
423
430
  }
424
431
 
425
- // ── B2: scrypt-based password profile (v0.1 write profile) ─────────
432
+ // ── B2: scrypt-based password profile (0.1 write profile) ─────────
426
433
  // Uses Node built-in crypto.scryptSync — zero additional dependencies.
427
- // v0.2 will promote the Argon2id profile (above) as the default write
434
+ // 0.2 will promote the Argon2id profile (above) as the default write
428
435
  // profile; this scrypt profile will remain a read-only legacy profile.
429
436
 
430
- const PASSWORD_PROTECTED_SCRYPT_PROFILE = 'kdna-password-protected-v1-scrypt';
437
+ const PASSWORD_PROTECTED_SCRYPT_PROFILE = 'kdna.encryption.password.scrypt';
431
438
  const SCRYPT_KDF = 'scrypt-sha256';
432
439
 
433
440
  function derivePasswordKeyScrypt(password, params = {}) {
@@ -468,6 +475,7 @@ function encryptProtectedEntryScrypt(plaintext, options = {}) {
468
475
 
469
476
  return {
470
477
  profile: PASSWORD_PROTECTED_SCRYPT_PROFILE,
478
+ profile_version: ENCRYPTION_PROFILE_VERSION,
471
479
  alg: ALG,
472
480
  kdf: SCRYPT_KDF,
473
481
  key_wrapping: RFC_KEY_WRAPPING,
@@ -508,6 +516,9 @@ function decryptProtectedEntryScrypt(envelopeValue, options = {}) {
508
516
  `unsupported encrypted entry profile: ${envelope.profile || 'unknown'} (expected ${PASSWORD_PROTECTED_SCRYPT_PROFILE})`,
509
517
  );
510
518
  }
519
+ if (envelope.profile_version !== ENCRYPTION_PROFILE_VERSION) {
520
+ throw new Error(`unsupported encrypted entry profile_version: ${envelope.profile_version || 'unknown'}`);
521
+ }
511
522
  if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
512
523
  if (envelope.kdf !== SCRYPT_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
513
524
  if (envelope.key_wrapping !== RFC_KEY_WRAPPING) {
@@ -537,95 +548,21 @@ function createPasswordDecryptEntryScrypt(options = {}) {
537
548
  decryptProtectedEntryScrypt(ciphertext, { entryName, manifest, password });
538
549
  }
539
550
 
540
- // ── Legacy / experimental profile (pre-RFC, backward compat) ───────
541
-
542
- function deriveLicensedEntryKeyLegacy(options = {}) {
543
- const { licenseKey, machineFingerprint, salt, keyLength = 32 } = options;
544
- if (!licenseKey) throw new Error('licenseKey is required');
545
- if (!machineFingerprint) throw new Error('machineFingerprint is required');
546
- const saltBuffer = Buffer.isBuffer(salt) || salt instanceof Uint8Array
547
- ? Buffer.from(salt)
548
- : decodeBase64(salt, 'salt');
549
- const secret = `${licenseKey}|${machineFingerprint}`;
550
- return crypto.scryptSync(secret, saltBuffer, keyLength);
551
- }
552
-
553
- function encryptLicensedEntryLegacy(plaintext, options = {}) {
554
- const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
555
- if (!entryName) throw new Error('entryName is required');
556
- const salt = crypto.randomBytes(16);
557
- const iv = crypto.randomBytes(12);
558
- const key = deriveLicensedEntryKeyLegacy({ licenseKey, machineFingerprint, salt });
559
- const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
560
- cipher.setAAD(encryptedEntryAad(entryName, manifest, LICENSED_EXPERIMENTAL_PROFILE));
561
- const ciphertext = Buffer.concat([cipher.update(toBuffer(plaintext, 'plaintext')), cipher.final()]);
562
- return {
563
- profile: LICENSED_EXPERIMENTAL_PROFILE,
564
- alg: ALG,
565
- kdf: LEGACY_KDF,
566
- salt: salt.toString('base64'),
567
- iv: iv.toString('base64'),
568
- tag: cipher.getAuthTag().toString('base64'),
569
- ciphertext: ciphertext.toString('base64'),
570
- };
571
- }
572
-
573
- function decryptLicensedEntryLegacy(envelopeValue, options = {}) {
574
- const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
575
- if (!entryName) throw new Error('entryName is required');
576
- const envelope = normalizeEnvelope(envelopeValue);
577
- if (envelope.profile !== LICENSED_EXPERIMENTAL_PROFILE) {
578
- throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'}`);
579
- }
580
- if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
581
- if (envelope.kdf !== LEGACY_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
582
- const key = deriveLicensedEntryKeyLegacy({
583
- licenseKey,
584
- machineFingerprint,
585
- salt: envelope.salt,
586
- });
587
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, decodeBase64(envelope.iv, 'iv'));
588
- decipher.setAAD(encryptedEntryAad(entryName, manifest, LICENSED_EXPERIMENTAL_PROFILE));
589
- decipher.setAuthTag(decodeBase64(envelope.tag, 'tag'));
590
- return Buffer.concat([
591
- decipher.update(decodeBase64(envelope.ciphertext, 'ciphertext')),
592
- decipher.final(),
593
- ]);
594
- }
595
-
596
- // ── Unified entry points (auto-detect profile) ────────────────────
597
-
598
- function encryptLicensedEntry(plaintext, options = {}) {
599
- return encryptLicensedEntryV1(plaintext, options);
600
- }
601
-
602
- function decryptLicensedEntry(envelopeValue, options = {}) {
603
- const envelope = normalizeEnvelope(envelopeValue);
604
- if (envelope.profile === LICENSED_ENTRY_PROFILE) {
605
- return decryptLicensedEntryV1(envelopeValue, options);
606
- }
607
- if (envelope.profile === LICENSED_EXPERIMENTAL_PROFILE) {
608
- return decryptLicensedEntryLegacy(envelopeValue, options);
609
- }
610
- throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'}`);
611
- }
612
-
613
551
  function createLicensedDecryptEntry(options = {}) {
614
- const { licenseKey, machineFingerprint } = options;
552
+ const { licenseKey } = options;
615
553
  return ({ entryName, ciphertext, manifest }) =>
616
- decryptLicensedEntry(ciphertext, { entryName, manifest, licenseKey, machineFingerprint });
554
+ decryptLicensedEntry(ciphertext, { entryName, manifest, licenseKey });
617
555
  }
618
556
 
619
557
  module.exports = {
620
558
  // Profiles
621
559
  LICENSED_ENTRY_PROFILE,
622
- LICENSED_EXPERIMENTAL_PROFILE,
623
560
  PASSWORD_PROTECTED_PROFILE,
624
561
  PASSWORD_PROTECTED_SCRYPT_PROFILE,
562
+ ENCRYPTION_PROFILE_VERSION,
625
563
  ALG,
626
564
  RFC_KDF,
627
565
  RFC_KEY_WRAPPING,
628
- LEGACY_KDF,
629
566
  PASSWORD_KDF,
630
567
  SCRYPT_KDF,
631
568
 
@@ -634,8 +571,8 @@ module.exports = {
634
571
  generateCEK,
635
572
  wrapCEK,
636
573
  unwrapCEK,
637
- encryptLicensedEntryV1,
638
- decryptLicensedEntryV1,
574
+ encryptLicensedEntry,
575
+ decryptLicensedEntry,
639
576
 
640
577
  // RFC-0009 compliant (Argon2id)
641
578
  derivePasswordKey,
@@ -646,20 +583,13 @@ module.exports = {
646
583
  createPasswordDecryptEntry,
647
584
  createRecoveryDecryptEntry,
648
585
 
649
- // B2 scrypt password profile (v0.1 write profile)
586
+ // B2 scrypt password profile (0.1 write profile)
650
587
  derivePasswordKeyScrypt,
651
588
  encryptProtectedEntryScrypt,
652
589
  decryptProtectedEntryScrypt,
653
590
  createPasswordDecryptEntryScrypt,
654
591
 
655
- // Legacy
656
- deriveLicensedEntryKey: deriveLicensedEntryKeyLegacy,
657
- encryptLicensedEntryLegacy,
658
- decryptLicensedEntryLegacy,
659
-
660
- // Unified
661
- encryptLicensedEntry,
662
- decryptLicensedEntry,
592
+ // Consumer adapter
663
593
  createLicensedDecryptEntry,
664
594
 
665
595
  // Low-level
@@ -8,26 +8,27 @@
8
8
 
9
9
  const crypto = require('node:crypto');
10
10
  const path = require('node:path');
11
- const Ajv2020 = require('ajv/dist/2020');
11
+ const JsonSchema2020 = require('ajv/dist/2020');
12
12
  const addFormats = require('ajv-formats');
13
13
  const cbor = require('cbor-x');
14
14
  const { hkdfSha256, aesWrap, aesUnwrap } = require('./crypto-profile');
15
15
 
16
- const EXTERNAL_ENVELOPE_PROFILE = 'kdna-envelope-external-grant-v1';
17
- const EXTERNAL_GRANT_PROFILE = 'kdna-key-grant-v1';
18
- const EXTERNAL_AAD_PROFILE = 'kdna-external-asset-cek-v1';
19
- const DEVICE_KEK_PROFILE = 'kdna-device-grant-kek-v1';
16
+ const EXTERNAL_ENVELOPE_PROFILE = 'kdna.envelope.external-grant';
17
+ const EXTERNAL_GRANT_PROFILE = 'kdna.grant.external-key';
18
+ const EXTERNAL_AAD_PROFILE = 'kdna.key-context.asset-content';
19
+ const DEVICE_KEK_PROFILE = 'kdna.key-context.device-grant';
20
+ const EXTERNAL_GRANT_CONTRACT_VERSION = '0.1.0';
20
21
  const ENVELOPE_ALG = 'A256GCM';
21
22
  const GRANT_WRAP_ALG = 'X25519-HKDF-SHA256+A256KW';
22
23
 
23
24
  const schemaRoot = path.join(__dirname, '..', 'schema');
24
- const ajv = new Ajv2020({ allErrors: true, strict: true });
25
+ const ajv = new JsonSchema2020({ allErrors: true, strict: true });
25
26
  addFormats(ajv);
26
27
  const validateEnvelopeSchema = ajv.compile(
27
- require(path.join(schemaRoot, 'kdna-envelope-external-grant-v1.schema.json')),
28
+ require(path.join(schemaRoot, 'external-grant-envelope.schema.json')),
28
29
  );
29
30
  const validateGrantSchema = ajv.compile(
30
- require(path.join(schemaRoot, 'kdna-key-grant-v1.schema.json')),
31
+ require(path.join(schemaRoot, 'external-key-grant.schema.json')),
31
32
  );
32
33
 
33
34
  const verifiedEntitlements = new WeakSet();
@@ -127,8 +128,9 @@ function externalEnvelopeAad({ manifest, entryName, plaintextDigest, keyRef, iss
127
128
  const entitlementProfile = manifest?.entitlement?.profile || '';
128
129
  const fields = [
129
130
  EXTERNAL_ENVELOPE_PROFILE,
131
+ EXTERNAL_GRANT_CONTRACT_VERSION,
130
132
  manifest?.asset_uid || '',
131
- manifest?.asset_id || manifest?.name || '',
133
+ manifest?.asset_id || '',
132
134
  manifest?.version || '',
133
135
  entryName,
134
136
  plaintextDigest,
@@ -210,6 +212,7 @@ function encryptExternalGrantEntry(plaintextValue, options = {}) {
210
212
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
211
213
  return {
212
214
  profile: EXTERNAL_ENVELOPE_PROFILE,
215
+ contract_version: EXTERNAL_GRANT_CONTRACT_VERSION,
213
216
  alg: ENVELOPE_ALG,
214
217
  cek_derivation: 'HKDF-SHA256',
215
218
  key_ref: keyRef,
@@ -327,9 +330,18 @@ function createExternalKeyGrant(options = {}) {
327
330
  shared = crypto.diffieHellman({ privateKey: ephemeral.privateKey, publicKey: deviceKey });
328
331
  const salt = wrapSalt ? Buffer.from(wrapSalt) : crypto.randomBytes(16);
329
332
  if (salt.length !== 16) fail('KDNA_GRANT_FORMAT_INVALID', 'grant wrap salt must be 16 bytes');
330
- kek = hkdfSha256(shared, salt, Buffer.from(`${DEVICE_KEK_PROFILE}\n${grantId}`, 'utf8'), 32);
333
+ kek = hkdfSha256(
334
+ shared,
335
+ salt,
336
+ Buffer.from(
337
+ `${DEVICE_KEK_PROFILE}\n${EXTERNAL_GRANT_CONTRACT_VERSION}\n${grantId}`,
338
+ 'utf8',
339
+ ),
340
+ 32,
341
+ );
331
342
  const grant = {
332
343
  profile: EXTERNAL_GRANT_PROFILE,
344
+ contract_version: EXTERNAL_GRANT_CONTRACT_VERSION,
333
345
  grant_id: grantId,
334
346
  issuer,
335
347
  signing_key_id: signingKeyId,
@@ -339,7 +351,7 @@ function createExternalKeyGrant(options = {}) {
339
351
  device_public_key: devicePublicKey,
340
352
  device_signing_public_key: deviceSigningPublicKey,
341
353
  asset: {
342
- asset_id: manifest.asset_id || manifest.name,
354
+ asset_id: manifest.asset_id,
343
355
  asset_uid: manifest.asset_uid,
344
356
  version: manifest.version,
345
357
  digest: assetDigest,
@@ -402,7 +414,7 @@ function authorizeExternalKeyGrant(options = {}) {
402
414
  grant,
403
415
  issuerPublicKeys,
404
416
  manifest,
405
- checksums,
417
+ expectedAssetDigest,
406
418
  envelope: envelopeValue,
407
419
  deviceAgreementKey,
408
420
  expectedAccountId,
@@ -433,10 +445,15 @@ function authorizeExternalKeyGrant(options = {}) {
433
445
  'KDNA_GRANT_DEVICE_MISMATCH',
434
446
  'device signing key',
435
447
  );
436
- expectEqual(grant.asset.asset_id, manifest.asset_id || manifest.name, 'KDNA_GRANT_ASSET_MISMATCH', 'asset ID');
448
+ expectEqual(grant.asset.asset_id, manifest.asset_id, 'KDNA_GRANT_ASSET_MISMATCH', 'asset ID');
437
449
  expectEqual(grant.asset.asset_uid, manifest.asset_uid, 'KDNA_GRANT_ASSET_MISMATCH', 'asset UID');
438
450
  expectEqual(grant.asset.version, manifest.version, 'KDNA_GRANT_ASSET_MISMATCH', 'asset version');
439
- expectEqual(grant.asset.digest, checksums?.asset_digest, 'KDNA_GRANT_DIGEST_MISMATCH', 'asset digest');
451
+ expectEqual(
452
+ grant.asset.digest,
453
+ expectedAssetDigest,
454
+ 'KDNA_GRANT_DIGEST_MISMATCH',
455
+ 'asset digest',
456
+ );
440
457
  expectEqual(grant.asset.entry_path, envelope.entry_path, 'KDNA_GRANT_ASSET_MISMATCH', 'entry path');
441
458
  expectEqual(grant.asset.key_ref, envelope.key_ref, 'KDNA_GRANT_ASSET_MISMATCH', 'key reference');
442
459
  expectEqual(grant.asset.issuer_key_id, envelope.issuer_key_id, 'KDNA_GRANT_ASSET_MISMATCH', 'issuer asset key');
@@ -454,7 +471,15 @@ function authorizeExternalKeyGrant(options = {}) {
454
471
  const ephemeralKey = publicKeyFromRaw('X25519', `x25519:${b64url(ephemeralRaw)}`);
455
472
  const shared = crypto.diffieHellman({ privateKey, publicKey: ephemeralKey });
456
473
  const salt = decodeB64url(grant.wrap.salt, 'grant wrap salt', 16);
457
- const kek = hkdfSha256(shared, salt, Buffer.from(`${DEVICE_KEK_PROFILE}\n${grant.grant_id}`, 'utf8'), 32);
474
+ const kek = hkdfSha256(
475
+ shared,
476
+ salt,
477
+ Buffer.from(
478
+ `${DEVICE_KEK_PROFILE}\n${EXTERNAL_GRANT_CONTRACT_VERSION}\n${grant.grant_id}`,
479
+ 'utf8',
480
+ ),
481
+ 32,
482
+ );
458
483
  let cek;
459
484
  try {
460
485
  cek = aesUnwrap(kek, decodeB64url(grant.wrap.wrapped_cek, 'wrapped CEK', 40));
@@ -543,6 +568,7 @@ module.exports = {
543
568
  EXTERNAL_GRANT_PROFILE,
544
569
  EXTERNAL_AAD_PROFILE,
545
570
  DEVICE_KEK_PROFILE,
571
+ EXTERNAL_GRANT_CONTRACT_VERSION,
546
572
  KDNAExternalGrantError,
547
573
  canonicalJson,
548
574
  grantSigningPayload,
package/src/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
  * @aikdna/kdna-core — CJS entry point
3
3
  */
4
4
  const loader = require('./loader');
5
- const lint = require('./lint-pure');
6
5
  const validate = require('./validate-pure');
7
6
  const render = require('./render');
8
7
  const compose = require('./compose');
@@ -11,13 +10,15 @@ const cryptoProfile = require('./crypto-profile');
11
10
  const externalKeyGrant = require('./external-key-grant');
12
11
  const publicApi = require('./public-api');
13
12
  const workpackPure = require('./workpack-pure');
14
- const v1 = require('./v1');
13
+ const container = require('./container');
15
14
  const runtimeApi = require('./runtime-api');
15
+ const runtimeCapsule = require('./runtime-capsule');
16
+ const runtimeContract = require('./runtime-contract');
17
+ const loaderCompatibility = require('./loader-compatibility');
16
18
 
17
19
  module.exports = {
18
20
  ...publicApi,
19
21
  ...loader,
20
- ...lint,
21
22
  ...validate,
22
23
  ...render,
23
24
  ...compose,
@@ -25,6 +26,12 @@ module.exports = {
25
26
  ...cryptoProfile,
26
27
  ...externalKeyGrant,
27
28
  ...workpackPure,
28
- ...v1,
29
+ ...container,
29
30
  ...runtimeApi,
31
+ ...runtimeCapsule,
32
+ ...runtimeContract,
33
+ STRICT_LOADER_VERSION: loaderCompatibility.STRICT_LOADER_VERSION,
34
+ parseLoaderVersion: loaderCompatibility.parseLoaderVersion,
35
+ compareLoaderVersions: loaderCompatibility.compareLoaderVersions,
36
+ assessLoaderCompatibility: loaderCompatibility.assessLoaderCompatibility,
30
37
  };