@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.
- package/CHANGELOG.md +227 -64
- package/README.md +114 -77
- package/package.json +8 -2
- package/schema/agent-host-capabilities.schema.json +62 -0
- package/schema/agent-host-receipt.schema.json +371 -0
- package/schema/agent-host-request.schema.json +223 -0
- package/schema/{bundle-profile-v1.schema.json → bundle-profile.schema.json} +9 -5
- package/schema/checksums.schema.json +42 -3
- package/schema/consumption-plan.schema.json +428 -0
- package/schema/digest-evidence.schema.json +212 -0
- package/schema/{kdna-envelope-external-grant-v1.schema.json → external-grant-envelope.schema.json} +17 -4
- package/schema/{kdna-key-grant-v1.schema.json → external-key-grant.schema.json} +39 -7
- package/schema/judgment-trace.schema.json +1251 -0
- package/schema/load-plan.schema.json +7 -5
- package/schema/manifest.schema.json +70 -10
- package/schema/{payload-profile-v1.schema.json → payload-profile.schema.json} +30 -6
- package/schema/runtime-capsule.schema.json +116 -0
- package/src/asset-reader.js +106 -120
- package/src/{v1 → container}/index.js +566 -209
- package/src/container/index.mjs +20 -0
- package/src/container-dispatcher.js +10 -7
- package/src/crypto-profile.js +37 -107
- package/src/external-key-grant.js +41 -15
- package/src/index.js +11 -4
- package/src/index.mjs +143 -24
- package/src/loader-compatibility.js +59 -0
- package/src/loader.js +1 -1
- package/src/public-api.js +59 -163
- package/src/remote-runtime.d.ts +13 -0
- package/src/remote-runtime.js +27 -0
- package/src/remote-runtime.mjs +3 -0
- package/src/runtime-api.js +9 -10
- package/src/runtime-capsule.js +529 -0
- package/src/runtime-contract.js +1882 -0
- package/src/types.d.ts +1127 -136
- package/src/workpack-pure.js +1 -1
- package/src/lint-pure.js +0 -430
- package/src/v1/index.mjs +0 -20
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
const JsonSchema2020 = require('ajv/dist/2020');
|
|
7
|
+
const addFormats = require('ajv-formats');
|
|
8
|
+
|
|
9
|
+
const { createKdnaAssetReader } = require('./asset-reader');
|
|
10
|
+
const container = require('./container');
|
|
11
|
+
const digestEvidenceSchema = require('../schema/digest-evidence.schema.json');
|
|
12
|
+
const runtimeCapsuleSchema = require('../schema/runtime-capsule.schema.json');
|
|
13
|
+
|
|
14
|
+
const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
const DIGEST_PROFILE = 'kdna.digest-evidence';
|
|
16
|
+
const DIGEST_PROFILE_VERSION = '0.1.0';
|
|
17
|
+
const CAPSULE_DIGEST_PROFILE = 'kdna.canonicalization.runtime-capsule-jcs';
|
|
18
|
+
const CANONICALIZATION_PROFILE_VERSION = '0.1.0';
|
|
19
|
+
const RUNTIME_CAPSULE_CONTRACT_VERSION = '0.1.0';
|
|
20
|
+
const COMPARISON_SOURCES = new Set([
|
|
21
|
+
'caller',
|
|
22
|
+
'registry',
|
|
23
|
+
'install_receipt',
|
|
24
|
+
'lockfile',
|
|
25
|
+
'kdna.json.content_digest',
|
|
26
|
+
'kdna.json.authoring.content_digest',
|
|
27
|
+
'checksums.json.entry_set_digest',
|
|
28
|
+
]);
|
|
29
|
+
const EXTERNAL_COMPARISON_SOURCES = new Set([
|
|
30
|
+
'caller',
|
|
31
|
+
'registry',
|
|
32
|
+
'install_receipt',
|
|
33
|
+
'lockfile',
|
|
34
|
+
]);
|
|
35
|
+
const COMPARISON_TARGETS = new Set([
|
|
36
|
+
'external_expected',
|
|
37
|
+
'manifest_declaration',
|
|
38
|
+
'checksum_declaration',
|
|
39
|
+
]);
|
|
40
|
+
const BASIS = Object.freeze({
|
|
41
|
+
asset: 'kdna.digest-basis.container-bytes',
|
|
42
|
+
content: 'kdna.digest-basis.content-tree',
|
|
43
|
+
runtime_entry_set: 'kdna.digest-basis.runtime-entry-set',
|
|
44
|
+
});
|
|
45
|
+
let validators;
|
|
46
|
+
|
|
47
|
+
function schemaValidators() {
|
|
48
|
+
if (validators) return validators;
|
|
49
|
+
const ajv = new JsonSchema2020({ allErrors: true, strict: false });
|
|
50
|
+
addFormats(ajv);
|
|
51
|
+
ajv.addSchema(digestEvidenceSchema);
|
|
52
|
+
validators = {
|
|
53
|
+
runtimeCapsule: ajv.compile(runtimeCapsuleSchema),
|
|
54
|
+
};
|
|
55
|
+
return validators;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fail(code, message, details) {
|
|
59
|
+
const error = new Error(message);
|
|
60
|
+
error.code = code;
|
|
61
|
+
if (details !== undefined) error.details = details;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sha256Digest(bytes) {
|
|
66
|
+
const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
67
|
+
return `sha256:${crypto.createHash('sha256').update(buffer).digest('hex')}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function snapshotPackagedBytes(input) {
|
|
71
|
+
if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
|
|
72
|
+
return { bytes: Buffer.from(input), inputKind: 'packaged_bytes' };
|
|
73
|
+
}
|
|
74
|
+
if (typeof input !== 'string') {
|
|
75
|
+
fail(
|
|
76
|
+
'KDNA_RUNTIME_CAPSULE_INPUT_INVALID',
|
|
77
|
+
'Runtime Capsule requires a packaged .kdna path, Buffer, or Uint8Array.',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let stat;
|
|
82
|
+
try {
|
|
83
|
+
stat = fs.statSync(input);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
fail('KDNA_RUNTIME_CAPSULE_INPUT_INVALID', `Cannot read packaged KDNA asset: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
if (stat.isDirectory()) {
|
|
88
|
+
fail(
|
|
89
|
+
'KDNA_ASSET_FILE_REQUIRED',
|
|
90
|
+
'Runtime Capsule requires final packaged .kdna bytes; authoring directories are not runtime assets.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!stat.isFile()) {
|
|
94
|
+
fail('KDNA_RUNTIME_CAPSULE_INPUT_INVALID', 'Runtime Capsule input is not a regular file.');
|
|
95
|
+
}
|
|
96
|
+
return { bytes: fs.readFileSync(input), inputKind: 'packaged_file' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function computeAssetDigest(assetBytes) {
|
|
100
|
+
if (!Buffer.isBuffer(assetBytes) && !(assetBytes instanceof Uint8Array)) {
|
|
101
|
+
fail('KDNA_DIGEST_INPUT_INVALID', 'A requires final packaged asset bytes.');
|
|
102
|
+
}
|
|
103
|
+
return sha256Digest(assetBytes);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readJsonEntry(asset, entryName) {
|
|
107
|
+
if (!asset.entries.has(entryName)) return null;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(asset.readEntry(entryName).toString('utf8'));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
fail('KDNA_DIGEST_DECLARATION_INVALID', `${entryName}: invalid JSON: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeExpectedDigest(
|
|
116
|
+
expected,
|
|
117
|
+
defaultSource,
|
|
118
|
+
allowedSources = EXTERNAL_COMPARISON_SOURCES,
|
|
119
|
+
) {
|
|
120
|
+
if (expected == null) return null;
|
|
121
|
+
const normalized =
|
|
122
|
+
typeof expected === 'string' ? { value: expected, source: defaultSource } : expected;
|
|
123
|
+
if (
|
|
124
|
+
!normalized ||
|
|
125
|
+
typeof normalized !== 'object' ||
|
|
126
|
+
!SHA256_RE.test(normalized.value || '') ||
|
|
127
|
+
typeof normalized.source !== 'string' ||
|
|
128
|
+
!allowedSources.has(normalized.source)
|
|
129
|
+
) {
|
|
130
|
+
fail(
|
|
131
|
+
'KDNA_DIGEST_EXPECTATION_INVALID',
|
|
132
|
+
'Expected digest must provide a lowercase sha256 value and a factual source.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return { value: normalized.value, source: normalized.source };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeDeclaredDigest(value, source) {
|
|
139
|
+
return normalizeExpectedDigest(
|
|
140
|
+
{ value, source },
|
|
141
|
+
source,
|
|
142
|
+
new Set([source]),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function comparison(observed, expected, against) {
|
|
147
|
+
if (!expected) {
|
|
148
|
+
return { state: 'not_compared', against: null, expected: null, source: null };
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
state: observed === expected.value ? 'matched' : 'mismatched',
|
|
152
|
+
against,
|
|
153
|
+
expected: expected.value,
|
|
154
|
+
source: expected.source,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function declaredContentDigest(manifest) {
|
|
159
|
+
const topLevel = manifest && manifest.content_digest !== undefined
|
|
160
|
+
? normalizeDeclaredDigest(manifest.content_digest, 'kdna.json.content_digest')
|
|
161
|
+
: null;
|
|
162
|
+
const authoring = manifest?.authoring?.content_digest !== undefined
|
|
163
|
+
? normalizeDeclaredDigest(
|
|
164
|
+
manifest.authoring.content_digest,
|
|
165
|
+
'kdna.json.authoring.content_digest',
|
|
166
|
+
)
|
|
167
|
+
: null;
|
|
168
|
+
if (topLevel && authoring && topLevel.value !== authoring.value) {
|
|
169
|
+
fail(
|
|
170
|
+
'KDNA_CONTENT_DIGEST_DECLARATION_CONFLICT',
|
|
171
|
+
'kdna.json content_digest conflicts with authoring.content_digest.',
|
|
172
|
+
{ top_level: topLevel.value, authoring: authoring.value },
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return topLevel || authoring;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function declaredEntrySetDigest(checksums) {
|
|
179
|
+
if (!checksums) return null;
|
|
180
|
+
return checksums.entry_set_digest !== undefined
|
|
181
|
+
? normalizeDeclaredDigest(
|
|
182
|
+
checksums.entry_set_digest,
|
|
183
|
+
'checksums.json.entry_set_digest',
|
|
184
|
+
)
|
|
185
|
+
: null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function comparisonWithDeclarationPriority(observed, declared, external, declarationTarget) {
|
|
189
|
+
const declarationComparison = comparison(observed, declared, declarationTarget);
|
|
190
|
+
if (declarationComparison.state === 'mismatched') return declarationComparison;
|
|
191
|
+
if (external) return comparison(observed, external, 'external_expected');
|
|
192
|
+
return declarationComparison;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function computeDigestEvidenceFromBytes(assetBytes, options = {}) {
|
|
196
|
+
const bytes = Buffer.isBuffer(assetBytes)
|
|
197
|
+
? Buffer.from(assetBytes)
|
|
198
|
+
: Buffer.from(assetBytes || []);
|
|
199
|
+
const reader = createKdnaAssetReader();
|
|
200
|
+
const asset = reader.openSync(bytes);
|
|
201
|
+
if (!asset.entries.has('kdna.json') || !asset.entries.has('payload.kdnab')) {
|
|
202
|
+
fail(
|
|
203
|
+
'KDNA_DIGEST_INPUT_INVALID',
|
|
204
|
+
'Digest evidence requires kdna.json and payload.kdnab entries.',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const manifest = readJsonEntry(asset, 'kdna.json');
|
|
209
|
+
const checksums = readJsonEntry(asset, 'checksums.json');
|
|
210
|
+
const assetDigest = computeAssetDigest(bytes);
|
|
211
|
+
const contentDigest = reader.contentDigestSync(asset);
|
|
212
|
+
const entrySetDigest = container.computeRuntimeEntrySetDigest(
|
|
213
|
+
asset.readEntry('kdna.json'),
|
|
214
|
+
asset.readEntry('payload.kdnab'),
|
|
215
|
+
);
|
|
216
|
+
const externalExpected = options.expectedDigests || {};
|
|
217
|
+
const expectedAsset = normalizeExpectedDigest(
|
|
218
|
+
externalExpected.asset,
|
|
219
|
+
'caller',
|
|
220
|
+
);
|
|
221
|
+
const externalContent = normalizeExpectedDigest(externalExpected.content, 'caller');
|
|
222
|
+
const externalEntrySet = normalizeExpectedDigest(externalExpected.runtime_entry_set, 'caller');
|
|
223
|
+
const declaredContent = declaredContentDigest(manifest);
|
|
224
|
+
const declaredEntrySet = declaredEntrySetDigest(checksums);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
profile: DIGEST_PROFILE,
|
|
228
|
+
profile_version: DIGEST_PROFILE_VERSION,
|
|
229
|
+
asset: {
|
|
230
|
+
value: assetDigest,
|
|
231
|
+
basis: BASIS.asset,
|
|
232
|
+
comparison: comparison(assetDigest, expectedAsset, 'external_expected'),
|
|
233
|
+
},
|
|
234
|
+
content: {
|
|
235
|
+
value: contentDigest,
|
|
236
|
+
basis: BASIS.content,
|
|
237
|
+
comparison: comparisonWithDeclarationPriority(
|
|
238
|
+
contentDigest,
|
|
239
|
+
declaredContent,
|
|
240
|
+
externalContent,
|
|
241
|
+
'manifest_declaration',
|
|
242
|
+
),
|
|
243
|
+
},
|
|
244
|
+
runtime_entry_set: {
|
|
245
|
+
value: entrySetDigest,
|
|
246
|
+
basis: BASIS.runtime_entry_set,
|
|
247
|
+
comparison: comparisonWithDeclarationPriority(
|
|
248
|
+
entrySetDigest,
|
|
249
|
+
declaredEntrySet,
|
|
250
|
+
externalEntrySet,
|
|
251
|
+
'checksum_declaration',
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function computeDigestEvidence(input, options = {}) {
|
|
258
|
+
const { bytes } = snapshotPackagedBytes(input);
|
|
259
|
+
return computeDigestEvidenceFromBytes(bytes, options);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function assertUnicodeScalarString(value, label) {
|
|
263
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
264
|
+
const code = value.charCodeAt(index);
|
|
265
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
266
|
+
const next = value.charCodeAt(index + 1);
|
|
267
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
268
|
+
fail('KDNA_JCS_INVALID_UNICODE', `${label} contains an unpaired high surrogate.`);
|
|
269
|
+
}
|
|
270
|
+
index += 1;
|
|
271
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
272
|
+
fail('KDNA_JCS_INVALID_UNICODE', `${label} contains an unpaired low surrogate.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function canonicalizeJcs(value) {
|
|
278
|
+
const active = new Set();
|
|
279
|
+
|
|
280
|
+
function serialize(current, label) {
|
|
281
|
+
if (current === null) return 'null';
|
|
282
|
+
if (typeof current === 'boolean') return current ? 'true' : 'false';
|
|
283
|
+
if (typeof current === 'string') {
|
|
284
|
+
assertUnicodeScalarString(current, label);
|
|
285
|
+
return JSON.stringify(current);
|
|
286
|
+
}
|
|
287
|
+
if (typeof current === 'number') {
|
|
288
|
+
if (!Number.isFinite(current)) {
|
|
289
|
+
fail('KDNA_JCS_NON_FINITE_NUMBER', `${label} contains a non-finite number.`);
|
|
290
|
+
}
|
|
291
|
+
return JSON.stringify(current);
|
|
292
|
+
}
|
|
293
|
+
if (typeof current !== 'object') {
|
|
294
|
+
fail('KDNA_JCS_UNSUPPORTED_VALUE', `${label} is not an RFC 8785 JSON value.`);
|
|
295
|
+
}
|
|
296
|
+
if (active.has(current)) {
|
|
297
|
+
fail('KDNA_JCS_CYCLIC_VALUE', `${label} contains a cyclic reference.`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
active.add(current);
|
|
301
|
+
try {
|
|
302
|
+
if (Array.isArray(current)) {
|
|
303
|
+
const items = [];
|
|
304
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
305
|
+
if (!Object.prototype.hasOwnProperty.call(current, index)) {
|
|
306
|
+
fail('KDNA_JCS_UNSUPPORTED_VALUE', `${label} contains a sparse array.`);
|
|
307
|
+
}
|
|
308
|
+
items.push(serialize(current[index], `${label}[${index}]`));
|
|
309
|
+
}
|
|
310
|
+
return `[${items.join(',')}]`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const prototype = Object.getPrototypeOf(current);
|
|
314
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
315
|
+
fail('KDNA_JCS_UNSUPPORTED_VALUE', `${label} must be a plain JSON object.`);
|
|
316
|
+
}
|
|
317
|
+
const names = Object.getOwnPropertyNames(current);
|
|
318
|
+
const keys = Object.keys(current);
|
|
319
|
+
if (names.length !== keys.length || Object.getOwnPropertySymbols(current).length !== 0) {
|
|
320
|
+
fail('KDNA_JCS_UNSUPPORTED_VALUE', `${label} contains non-JSON object members.`);
|
|
321
|
+
}
|
|
322
|
+
keys.sort();
|
|
323
|
+
const members = keys.map((key) => {
|
|
324
|
+
assertUnicodeScalarString(key, `${label} key`);
|
|
325
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
326
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
327
|
+
fail('KDNA_JCS_UNSUPPORTED_VALUE', `${label}.${key} must be a data property.`);
|
|
328
|
+
}
|
|
329
|
+
return `${JSON.stringify(key)}:${serialize(descriptor.value, `${label}.${key}`)}`;
|
|
330
|
+
});
|
|
331
|
+
return `{${members.join(',')}}`;
|
|
332
|
+
} finally {
|
|
333
|
+
active.delete(current);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return serialize(value, '$');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function computeCapsuleDeliveryDigest(capsule) {
|
|
341
|
+
return sha256Digest(Buffer.from(canonicalizeJcs(capsule), 'utf8'));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function schemaErrorDetails(validator) {
|
|
345
|
+
return JSON.parse(JSON.stringify(validator.errors || []));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function assertJsonValue(value, code, label) {
|
|
349
|
+
try {
|
|
350
|
+
canonicalizeJcs(value);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
fail(code, `${label} is not an RFC 8785 JSON value: ${error.message}`, {
|
|
353
|
+
cause_code: error.code || null,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function assertRuntimeCapsuleSuccess(capsule, code = 'KDNA_RUNTIME_CAPSULE_INVALID') {
|
|
359
|
+
assertJsonValue(capsule, code, 'Runtime Capsule');
|
|
360
|
+
const validator = schemaValidators().runtimeCapsule;
|
|
361
|
+
if (!validator(capsule)) {
|
|
362
|
+
fail(
|
|
363
|
+
code,
|
|
364
|
+
'Runtime Capsule does not satisfy the current contract.',
|
|
365
|
+
schemaErrorDetails(validator),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
assertSuccessfulDigestEvidence(capsule.digests);
|
|
369
|
+
if (capsule.signature.state !== capsule.trace.signature_state) {
|
|
370
|
+
fail(code, 'Runtime Capsule signature state conflicts with its trace.');
|
|
371
|
+
}
|
|
372
|
+
if (capsule.profile !== capsule.trace.profile) {
|
|
373
|
+
fail(code, 'Runtime Capsule profile conflicts with its trace.');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function assertSuccessfulDigestEvidence(digests) {
|
|
378
|
+
if (
|
|
379
|
+
!digests ||
|
|
380
|
+
digests.profile !== DIGEST_PROFILE ||
|
|
381
|
+
digests.profile_version !== DIGEST_PROFILE_VERSION
|
|
382
|
+
) {
|
|
383
|
+
fail(
|
|
384
|
+
'KDNA_RUNTIME_CAPSULE_DIGEST_EVIDENCE_INVALID',
|
|
385
|
+
'Runtime Capsule digest evidence is missing or uses the wrong profile coordinate.',
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
for (const name of ['asset', 'content', 'runtime_entry_set']) {
|
|
389
|
+
const item = digests[name];
|
|
390
|
+
if (!item || !SHA256_RE.test(item.value || '') || item.basis !== BASIS[name]) {
|
|
391
|
+
fail(
|
|
392
|
+
'KDNA_RUNTIME_CAPSULE_DIGEST_EVIDENCE_INVALID',
|
|
393
|
+
`Runtime Capsule ${name} digest evidence is invalid.`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const comparisonState = item.comparison && item.comparison.state;
|
|
397
|
+
if (!['matched', 'not_compared'].includes(comparisonState)) {
|
|
398
|
+
const mismatchCodes = {
|
|
399
|
+
asset: 'KDNA_ASSET_DIGEST_MISMATCH',
|
|
400
|
+
content: 'KDNA_CONTENT_DIGEST_MISMATCH',
|
|
401
|
+
runtime_entry_set: 'KDNA_RUNTIME_ENTRY_SET_DIGEST_MISMATCH',
|
|
402
|
+
};
|
|
403
|
+
fail(
|
|
404
|
+
comparisonState === 'mismatched'
|
|
405
|
+
? mismatchCodes[name]
|
|
406
|
+
: 'KDNA_RUNTIME_CAPSULE_DIGEST_EVIDENCE_INVALID',
|
|
407
|
+
`Runtime Capsule cannot be emitted with ${name} comparison ${comparisonState || 'missing'}.`,
|
|
408
|
+
{ digest: name, evidence: item },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (
|
|
412
|
+
comparisonState === 'matched' &&
|
|
413
|
+
(item.comparison.expected !== item.value ||
|
|
414
|
+
!COMPARISON_TARGETS.has(item.comparison.against) ||
|
|
415
|
+
!COMPARISON_SOURCES.has(item.comparison.source))
|
|
416
|
+
) {
|
|
417
|
+
fail(
|
|
418
|
+
'KDNA_RUNTIME_CAPSULE_DIGEST_EVIDENCE_INVALID',
|
|
419
|
+
`Runtime Capsule ${name} matched comparison is internally inconsistent.`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (
|
|
423
|
+
comparisonState === 'not_compared' &&
|
|
424
|
+
(item.comparison.expected !== null ||
|
|
425
|
+
item.comparison.against !== null ||
|
|
426
|
+
item.comparison.source !== null)
|
|
427
|
+
) {
|
|
428
|
+
fail(
|
|
429
|
+
'KDNA_RUNTIME_CAPSULE_DIGEST_EVIDENCE_INVALID',
|
|
430
|
+
`Runtime Capsule ${name} not_compared evidence must not claim an expectation.`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function normalizeRuntimeAccess(access) {
|
|
437
|
+
return access === undefined ? 'public' : access;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function buildRuntimeCapsule({
|
|
441
|
+
projection,
|
|
442
|
+
manifest,
|
|
443
|
+
digests,
|
|
444
|
+
signature,
|
|
445
|
+
inputKind,
|
|
446
|
+
loadedAt,
|
|
447
|
+
schemaValid,
|
|
448
|
+
} = {}) {
|
|
449
|
+
if (!projection || typeof projection !== 'object' || Array.isArray(projection)) {
|
|
450
|
+
fail('KDNA_RUNTIME_CAPSULE_BUILD_INVALID', 'Runtime Capsule builder requires a projection.');
|
|
451
|
+
}
|
|
452
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
453
|
+
fail('KDNA_RUNTIME_CAPSULE_BUILD_INVALID', 'Runtime Capsule builder requires the manifest.');
|
|
454
|
+
}
|
|
455
|
+
for (const field of ['asset_id', 'asset_uid', 'version', 'judgment_version']) {
|
|
456
|
+
if (typeof manifest[field] !== 'string' || manifest[field].length === 0) {
|
|
457
|
+
fail('KDNA_RUNTIME_CAPSULE_BUILD_INVALID', `Runtime manifest ${field} is required.`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!['packaged_file', 'packaged_bytes'].includes(inputKind)) {
|
|
461
|
+
fail(
|
|
462
|
+
'KDNA_RUNTIME_CAPSULE_BUILD_INVALID',
|
|
463
|
+
'Runtime Capsule input_kind must identify packaged bytes.',
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
assertSuccessfulDigestEvidence(digests);
|
|
467
|
+
const timestamp = loadedAt || new Date().toISOString();
|
|
468
|
+
if (typeof timestamp !== 'string' || Number.isNaN(Date.parse(timestamp))) {
|
|
469
|
+
fail(
|
|
470
|
+
'KDNA_RUNTIME_CAPSULE_BUILD_INVALID',
|
|
471
|
+
'Runtime Capsule loaded_at must be an ISO date-time string.',
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const runtimeAccess = normalizeRuntimeAccess(manifest.access);
|
|
475
|
+
if (!['public', 'licensed', 'remote'].includes(runtimeAccess)) {
|
|
476
|
+
fail('KDNA_RUNTIME_CAPSULE_BUILD_INVALID', `Unsupported Runtime access: ${runtimeAccess}.`);
|
|
477
|
+
}
|
|
478
|
+
const signatureEvidence = signature || { state: 'absent' };
|
|
479
|
+
|
|
480
|
+
const capsule = {
|
|
481
|
+
type: 'kdna.runtime-capsule',
|
|
482
|
+
contract_version: RUNTIME_CAPSULE_CONTRACT_VERSION,
|
|
483
|
+
asset: {
|
|
484
|
+
asset_id: manifest.asset_id,
|
|
485
|
+
asset_uid: manifest.asset_uid,
|
|
486
|
+
version: manifest.version,
|
|
487
|
+
judgment_version: manifest.judgment_version,
|
|
488
|
+
},
|
|
489
|
+
digests,
|
|
490
|
+
signature: signatureEvidence,
|
|
491
|
+
access: runtimeAccess,
|
|
492
|
+
risk_level: manifest.risk_level || null,
|
|
493
|
+
profile: projection.profile,
|
|
494
|
+
context: projection.content || {},
|
|
495
|
+
trace: {
|
|
496
|
+
payload_encoding: manifest.payload?.encoding || 'cbor',
|
|
497
|
+
loaded_by: 'kdna-core',
|
|
498
|
+
loaded_at: timestamp,
|
|
499
|
+
input_kind: inputKind,
|
|
500
|
+
runtime_eligible: true,
|
|
501
|
+
schema_valid: schemaValid === true,
|
|
502
|
+
signature_state: signatureEvidence.state,
|
|
503
|
+
profile: projection.profile,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
assertRuntimeCapsuleSuccess(capsule, 'KDNA_RUNTIME_CAPSULE_BUILD_INVALID');
|
|
507
|
+
return capsule;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function loadRuntimeCapsule(input, options = {}) {
|
|
511
|
+
snapshotPackagedBytes(input);
|
|
512
|
+
return container.loadAuthorized(input, { ...options, as: 'json' });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
module.exports = {
|
|
516
|
+
BASIS,
|
|
517
|
+
CAPSULE_DIGEST_PROFILE,
|
|
518
|
+
CANONICALIZATION_PROFILE_VERSION,
|
|
519
|
+
DIGEST_PROFILE,
|
|
520
|
+
DIGEST_PROFILE_VERSION,
|
|
521
|
+
RUNTIME_CAPSULE_CONTRACT_VERSION,
|
|
522
|
+
buildRuntimeCapsule,
|
|
523
|
+
canonicalizeJcs,
|
|
524
|
+
computeAssetDigest,
|
|
525
|
+
computeCapsuleDeliveryDigest,
|
|
526
|
+
computeDigestEvidence,
|
|
527
|
+
computeRuntimeEntrySetDigest: container.computeRuntimeEntrySetDigest,
|
|
528
|
+
loadRuntimeCapsule,
|
|
529
|
+
};
|