enigma-memory 0.1.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/LICENSE +55 -0
- package/README.md +379 -0
- package/apps/cli/bin/enigma.mjs +1117 -0
- package/apps/desktop/src/app.js +1327 -0
- package/apps/desktop/src/index.html +1373 -0
- package/apps/desktop/src/styles.css +386 -0
- package/apps/gateway/bin/enigma-gateway.mjs +52 -0
- package/apps/gateway/src/server.mjs +1403 -0
- package/apps/native-host/README.md +265 -0
- package/apps/native-host/bin/enigma-native-host.mjs +499 -0
- package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
- package/apps/relay/bin/enigma-relay.mjs +52 -0
- package/apps/relay/src/server.mjs +1571 -0
- package/apps/verifier/bin/enigma-verify.mjs +240 -0
- package/deploy/docker-compose.production.example.yml +150 -0
- package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
- package/package.json +196 -0
- package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
- package/packages/adapters/src/index.js +183 -0
- package/packages/boundary/src/index.js +468 -0
- package/packages/connectors/src/index.js +533 -0
- package/packages/core/src/index.js +766 -0
- package/packages/enterprise/src/index.js +505 -0
- package/packages/importers/src/index.js +1083 -0
- package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
- package/packages/mcp-server/src/index.js +1185 -0
- package/packages/mesh/src/index.js +531 -0
- package/packages/metering/PACKAGE_CONTRACT.md +20 -0
- package/packages/metering/src/index.js +246 -0
- package/packages/optimizer/src/index.js +707 -0
- package/packages/passport/src/index.js +651 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
- package/packages/settlement/src/index.js +369 -0
- package/packages/storage/PACKAGE_CONTRACT.md +23 -0
- package/packages/storage/src/index.js +392 -0
- package/packages/vault/src/index.js +904 -0
- package/scripts/build-ai-orchestration-plan.mjs +248 -0
- package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
- package/scripts/build-cloudflare-token-policy.mjs +239 -0
- package/scripts/build-cloudflare-token-request.mjs +314 -0
- package/scripts/build-demo-assets.mjs +263 -0
- package/scripts/build-edge-backend-workers.mjs +353 -0
- package/scripts/build-goal-completion-audit.mjs +398 -0
- package/scripts/build-hosted-probe-worker.mjs +191 -0
- package/scripts/build-operator-acceptance-packet.mjs +254 -0
- package/scripts/build-operator-evidence-starter.mjs +525 -0
- package/scripts/build-production-backend-env-kit.mjs +384 -0
- package/scripts/build-production-dependency-report.mjs +369 -0
- package/scripts/build-production-handoff-packet.mjs +475 -0
- package/scripts/build-production-readiness-manifest.mjs +209 -0
- package/scripts/build-production-status-board.mjs +297 -0
- package/scripts/build-production-storage-migration.mjs +86 -0
- package/scripts/build-production-workplan.mjs +371 -0
- package/scripts/build-review-packet.mjs +594 -0
- package/scripts/check.mjs +419 -0
- package/scripts/cloudflare-ops.mjs +1672 -0
- package/scripts/cloudflare-secret-env.mjs +128 -0
- package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
- package/scripts/infrastructure-readiness.mjs +944 -0
- package/scripts/install-enigma-local.mjs +270 -0
- package/scripts/memory-optimization-benchmark.mjs +242 -0
- package/scripts/monitor-live-endpoints.mjs +358 -0
- package/scripts/release-audit.mjs +5588 -0
- package/scripts/release-provenance.mjs +250 -0
- package/scripts/run-backend-readiness-smoke.mjs +220 -0
- package/scripts/validate-backup-restore-drill.mjs +166 -0
- package/scripts/validate-cloudflare-credentials.mjs +112 -0
- package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
- package/scripts/validate-domain-tls.mjs +248 -0
- package/scripts/validate-hosted-backend-live.mjs +319 -0
- package/scripts/validate-incident-drill.mjs +221 -0
- package/scripts/validate-kms-custody.mjs +235 -0
- package/scripts/validate-legal-compliance-approval.mjs +274 -0
- package/scripts/validate-monitoring-alerting.mjs +263 -0
- package/scripts/validate-network-access-policy.mjs +285 -0
- package/scripts/validate-operator-acceptance.mjs +298 -0
- package/scripts/validate-production-manifests.mjs +325 -0
- package/scripts/validate-public-site-security.mjs +252 -0
- package/scripts/validate-security-threat-model.mjs +285 -0
- package/scripts/validate-service-settlement.mjs +256 -0
- package/scripts/validate-support-sla.mjs +190 -0
- package/scripts/validate-tenant-policy-approval.mjs +228 -0
- package/scripts/validate-usage-metering.mjs +224 -0
- package/scripts/validate-whitepaper-claims.mjs +166 -0
- package/specs/boundary-manifest-v1.schema.json +35 -0
- package/specs/capsule-v1.schema.json +55 -0
- package/specs/claim-boundary-manifest-v1.schema.json +22 -0
- package/specs/connector-profile-v1.schema.json +62 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -0
- package/specs/enterprise-policy-v1.schema.json +59 -0
- package/specs/gateway-request-v1.schema.json +32 -0
- package/specs/goal-completion-audit-v1.schema.json +70 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
- package/specs/import-report-v1.schema.json +214 -0
- package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
- package/specs/memory-event-v1.schema.json +42 -0
- package/specs/mesh-node-v1.schema.json +126 -0
- package/specs/passport-v1.schema.json +50 -0
- package/specs/production-handoff-packet-v1.schema.json +149 -0
- package/specs/receipt-v1.schema.json +61 -0
- package/specs/relay-record-v1.schema.json +43 -0
- package/specs/state-checkpoint-v1.schema.json +37 -0
- package/specs/trust-bundle-v1.schema.json +56 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
createHmac,
|
|
4
|
+
createPublicKey,
|
|
5
|
+
generateKeyPairSync,
|
|
6
|
+
sign as ed25519Sign,
|
|
7
|
+
verify as ed25519Verify
|
|
8
|
+
} from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
export const SHA256_PREFIX = 'sha256:';
|
|
11
|
+
export const EMPTY_MERKLE_ROOT = `${SHA256_PREFIX}${sha256Hex('enigma.merkle.empty.v1')}`;
|
|
12
|
+
|
|
13
|
+
const RECEIPT_SCHEMA = 'enigma.receipt.v1';
|
|
14
|
+
const CHECKPOINT_SCHEMA = 'enigma.state_checkpoint.v1';
|
|
15
|
+
const ED25519 = 'Ed25519';
|
|
16
|
+
const GENESIS = 'GENESIS';
|
|
17
|
+
const ROOT_RE = /^sha256:[a-f0-9]{64}$/;
|
|
18
|
+
const HASH_RE = /^[a-f0-9]{64}$/;
|
|
19
|
+
const RECEIPT_TOP_LEVEL_FIELDS = new Set([
|
|
20
|
+
'schema',
|
|
21
|
+
'receipt_id',
|
|
22
|
+
'event_hash',
|
|
23
|
+
'operation',
|
|
24
|
+
'tenant_id',
|
|
25
|
+
'subject_id',
|
|
26
|
+
'memory_addr',
|
|
27
|
+
'source_addr',
|
|
28
|
+
'provider',
|
|
29
|
+
'model',
|
|
30
|
+
'policy_id',
|
|
31
|
+
'sequence',
|
|
32
|
+
'previous_receipt_hash',
|
|
33
|
+
'active_set_root',
|
|
34
|
+
'receipt_log_root',
|
|
35
|
+
'timestamp',
|
|
36
|
+
'signer',
|
|
37
|
+
'signature'
|
|
38
|
+
]);
|
|
39
|
+
const RECEIPT_SIGNATURE_FIELDS = new Set(['alg', 'value']);
|
|
40
|
+
const RECEIPT_PUBLIC_PLAINTEXT_KEYS = new Set(['body', 'content', 'plaintext', 'text', 'memory', 'prompt', 'response', 'raw_memory']);
|
|
41
|
+
|
|
42
|
+
export function canonicalize(value) {
|
|
43
|
+
return canonicalizeValue(value, new WeakSet());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canonicalizeValue(value, seen) {
|
|
47
|
+
if (value === null) return 'null';
|
|
48
|
+
|
|
49
|
+
switch (typeof value) {
|
|
50
|
+
case 'string':
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
case 'number':
|
|
53
|
+
if (!Number.isFinite(value)) throw new TypeError('canonical JSON cannot encode non-finite numbers');
|
|
54
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
55
|
+
case 'boolean':
|
|
56
|
+
return value ? 'true' : 'false';
|
|
57
|
+
case 'undefined':
|
|
58
|
+
case 'function':
|
|
59
|
+
case 'symbol':
|
|
60
|
+
case 'bigint':
|
|
61
|
+
throw new TypeError(`canonical JSON cannot encode ${typeof value}`);
|
|
62
|
+
case 'object':
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
throw new TypeError(`canonical JSON cannot encode ${typeof value}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (seen.has(value)) throw new TypeError('canonical JSON cannot encode circular structures');
|
|
69
|
+
seen.add(value);
|
|
70
|
+
try {
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
const items = new Array(value.length);
|
|
73
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
74
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
75
|
+
throw new TypeError('canonical JSON cannot encode sparse arrays');
|
|
76
|
+
}
|
|
77
|
+
items[index] = canonicalizeValue(value[index], seen);
|
|
78
|
+
}
|
|
79
|
+
return `[${items.join(',')}]`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const prototype = Object.getPrototypeOf(value);
|
|
83
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
84
|
+
throw new TypeError('canonical JSON only accepts plain objects, arrays, and primitives');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const keys = Object.keys(value).sort();
|
|
88
|
+
const fields = new Array(keys.length);
|
|
89
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
90
|
+
const key = keys[index];
|
|
91
|
+
fields[index] = `${JSON.stringify(key)}:${canonicalizeValue(value[key], seen)}`;
|
|
92
|
+
}
|
|
93
|
+
return `{${fields.join(',')}}`;
|
|
94
|
+
} finally {
|
|
95
|
+
seen.delete(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function sha256Hex(value) {
|
|
100
|
+
return createHash('sha256').update(bytesForHash(value)).digest('hex');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function hmacSha256Hex(keyOrArgs, value) {
|
|
104
|
+
const args = isPlainRecord(keyOrArgs) && Object.prototype.hasOwnProperty.call(keyOrArgs, 'key')
|
|
105
|
+
? keyOrArgs
|
|
106
|
+
: { key: keyOrArgs, value };
|
|
107
|
+
if (args.key === undefined) throw new TypeError('hmacSha256Hex requires a key');
|
|
108
|
+
return createHmac('sha256', bytesForKey(args.key)).update(bytesForHash(args.value)).digest('hex');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function generateSigningKeyPair(options = {}) {
|
|
112
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
|
|
113
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
114
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
115
|
+
});
|
|
116
|
+
const key_id = options.key_id ?? options.keyId ?? `ed25519:${sha256Hex(publicKey).slice(0, 32)}`;
|
|
117
|
+
return {
|
|
118
|
+
alg: ED25519,
|
|
119
|
+
key_id,
|
|
120
|
+
signer: { key_id, alg: ED25519 },
|
|
121
|
+
publicKey,
|
|
122
|
+
privateKey
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function signPayload(payloadOrArgs, maybePrivateKey) {
|
|
127
|
+
const args = isPlainRecord(payloadOrArgs) && Object.prototype.hasOwnProperty.call(payloadOrArgs, 'payload')
|
|
128
|
+
? payloadOrArgs
|
|
129
|
+
: { payload: payloadOrArgs, privateKey: maybePrivateKey };
|
|
130
|
+
if (args.privateKey === undefined) throw new TypeError('signPayload requires a privateKey');
|
|
131
|
+
const signature = ed25519Sign(null, bytesForSigning(args.payload), args.privateKey);
|
|
132
|
+
return signature.toString('base64url');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function verifySignature(payloadOrArgs, maybeSignature, maybePublicKey) {
|
|
136
|
+
const args = isPlainRecord(payloadOrArgs) && Object.prototype.hasOwnProperty.call(payloadOrArgs, 'payload')
|
|
137
|
+
? payloadOrArgs
|
|
138
|
+
: { payload: payloadOrArgs, signature: maybeSignature, publicKey: maybePublicKey };
|
|
139
|
+
if (args.publicKey === undefined || args.signature === undefined) return false;
|
|
140
|
+
try {
|
|
141
|
+
return ed25519Verify(null, bytesForSigning(args.payload), args.publicKey, decodeSignature(args.signature));
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function receiptHash(receipt) {
|
|
148
|
+
return prefixedSha256(canonicalize(receipt));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createReceipt(receiptOrArgs, maybeOptions = {}) {
|
|
152
|
+
const options = normalizeCreateReceiptArgs(receiptOrArgs, maybeOptions);
|
|
153
|
+
const source = options.event ?? options;
|
|
154
|
+
const sequence = requiredInteger(options.sequence ?? source.sequence, 'sequence');
|
|
155
|
+
const previous_receipt_hash = normalizePreviousHash(
|
|
156
|
+
options.previous_receipt_hash ?? options.previousReceiptHash ?? (sequence === 0 ? GENESIS : undefined)
|
|
157
|
+
);
|
|
158
|
+
const signer = normalizeSigner(options.signer, options.key_id ?? options.keyId, options.privateKey);
|
|
159
|
+
const event_hash = normalizeSha256Root(options.event_hash ?? options.eventHash ?? source.event_hash ?? eventHash(source), 'event_hash');
|
|
160
|
+
const active_set_root = normalizeSha256Root(options.active_set_root ?? options.activeSetRoot ?? EMPTY_MERKLE_ROOT, 'active_set_root');
|
|
161
|
+
const receipt_log_root = normalizeSha256Root(options.receipt_log_root ?? options.receiptLogRoot ?? EMPTY_MERKLE_ROOT, 'receipt_log_root');
|
|
162
|
+
const operation = requiredString(options.operation ?? source.operation, 'operation');
|
|
163
|
+
const timestamp = requiredString(options.timestamp ?? source.timestamp ?? new Date().toISOString(), 'timestamp');
|
|
164
|
+
|
|
165
|
+
const body = {
|
|
166
|
+
schema: RECEIPT_SCHEMA,
|
|
167
|
+
event_hash,
|
|
168
|
+
operation,
|
|
169
|
+
tenant_id: requiredString(options.tenant_id ?? options.tenantId ?? source.tenant_id ?? source.tenantId, 'tenant_id'),
|
|
170
|
+
subject_id: requiredString(options.subject_id ?? options.subjectId ?? source.subject_id ?? source.subjectId, 'subject_id'),
|
|
171
|
+
sequence,
|
|
172
|
+
previous_receipt_hash,
|
|
173
|
+
active_set_root,
|
|
174
|
+
receipt_log_root,
|
|
175
|
+
timestamp,
|
|
176
|
+
signer
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
copyOptional(body, options, source, 'memory_addr', 'memoryAddr');
|
|
180
|
+
copyOptional(body, options, source, 'source_addr', 'sourceAddr');
|
|
181
|
+
copyOptional(body, options, source, 'provider', 'provider');
|
|
182
|
+
copyOptional(body, options, source, 'model', 'model');
|
|
183
|
+
copyOptional(body, options, source, 'policy_id', 'policyId');
|
|
184
|
+
|
|
185
|
+
const receipt_id = options.receipt_id ?? options.receiptId ?? `rcpt_${sha256Hex(canonicalize(orderReceiptFields({ ...body, receipt_id: '' }))).slice(0, 32)}`;
|
|
186
|
+
const unsigned = orderReceiptFields({ ...body, receipt_id });
|
|
187
|
+
const signature = options.signature ?? {
|
|
188
|
+
alg: ED25519,
|
|
189
|
+
value: signPayload({ payload: unsigned, privateKey: options.privateKey })
|
|
190
|
+
};
|
|
191
|
+
return orderReceiptFields({ ...unsigned, signature: normalizeReceiptSignature(signature) });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function verifyReceipt(receiptOrArgs, maybePublicKey, maybeOptions = {}) {
|
|
195
|
+
const { receipt, options } = normalizeVerifyReceiptArgs(receiptOrArgs, maybePublicKey, maybeOptions);
|
|
196
|
+
const errors = [];
|
|
197
|
+
|
|
198
|
+
if (!isPlainRecord(receipt)) {
|
|
199
|
+
return verificationResult(false, ['receipt must be an object']);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
validateReceiptShape(receipt, errors);
|
|
203
|
+
|
|
204
|
+
if (receipt.schema !== RECEIPT_SCHEMA) errors.push('schema mismatch');
|
|
205
|
+
if (!isPlainRecord(receipt.signer) || receipt.signer.alg !== ED25519 || !receipt.signer.key_id) errors.push('signer mismatch');
|
|
206
|
+
if (!isPlainRecord(receipt.signature) || receipt.signature.alg !== ED25519 || !receipt.signature.value) errors.push('signature missing');
|
|
207
|
+
if (!Number.isInteger(receipt.sequence) || receipt.sequence < 0) errors.push('sequence must be a non-negative integer');
|
|
208
|
+
if (!ROOT_RE.test(receipt.event_hash ?? '')) errors.push('event_hash must be a sha256 root');
|
|
209
|
+
if (!ROOT_RE.test(receipt.active_set_root ?? '')) errors.push('active_set_root must be a sha256 root');
|
|
210
|
+
if (!ROOT_RE.test(receipt.receipt_log_root ?? '')) errors.push('receipt_log_root must be a sha256 root');
|
|
211
|
+
if (!isPreviousHash(receipt.previous_receipt_hash)) errors.push('previous_receipt_hash mismatch');
|
|
212
|
+
|
|
213
|
+
const expectedSigner = options.expectedSignerKeyId ?? options.expectedSigner ?? options.signer_key_id;
|
|
214
|
+
if (expectedSigner !== undefined && receipt.signer?.key_id !== expectedSigner) errors.push('wrong signer');
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const derivedId = deriveReceiptId(receipt);
|
|
218
|
+
if (receipt.receipt_id !== derivedId) errors.push('receipt_id mismatch');
|
|
219
|
+
} catch {
|
|
220
|
+
errors.push('receipt_id mismatch');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const publicKey = resolvePublicKey(receipt.signer?.key_id, options);
|
|
224
|
+
if (publicKey === undefined) {
|
|
225
|
+
errors.push('missing public key');
|
|
226
|
+
} else {
|
|
227
|
+
try {
|
|
228
|
+
if (!verifySignature({ payload: unsignedReceipt(receipt), signature: receipt.signature, publicKey })) {
|
|
229
|
+
errors.push('signature mismatch');
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
errors.push('signature mismatch');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const ok = errors.length === 0;
|
|
237
|
+
return verificationResult(ok, errors, { receipt_hash: safeReceiptHash(receipt), signer_key_id: receipt.signer?.key_id });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function verifyReceiptChain(receiptsOrArgs, maybeOptions = {}) {
|
|
241
|
+
const options = Array.isArray(receiptsOrArgs) ? maybeOptions : receiptsOrArgs ?? {};
|
|
242
|
+
const receipts = Array.isArray(receiptsOrArgs) ? receiptsOrArgs : options.receipts;
|
|
243
|
+
const errors = [];
|
|
244
|
+
|
|
245
|
+
if (!Array.isArray(receipts)) return verificationResult(false, ['receipts must be an array']);
|
|
246
|
+
|
|
247
|
+
const expectedStartSequence = options.startSequence ?? options.expectedStartSequence ?? 0;
|
|
248
|
+
let expectedPrevious = options.previous_receipt_hash ?? options.previousReceiptHash ?? (expectedStartSequence === 0 ? GENESIS : undefined);
|
|
249
|
+
if (expectedPrevious === undefined) errors.push('previous receipt hash is required for non-genesis chain segments');
|
|
250
|
+
|
|
251
|
+
const hashes = [];
|
|
252
|
+
for (let index = 0; index < receipts.length; index += 1) {
|
|
253
|
+
const receipt = receipts[index];
|
|
254
|
+
const verified = verifyReceipt({ receipt, ...options });
|
|
255
|
+
if (!verified.ok) {
|
|
256
|
+
for (const error of verified.errors) errors.push(`receipt ${index}: ${error}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const expectedSequence = expectedStartSequence + index;
|
|
260
|
+
if (receipt?.sequence !== expectedSequence) {
|
|
261
|
+
errors.push(`receipt ${index}: sequence gap or reorder`);
|
|
262
|
+
}
|
|
263
|
+
if (expectedPrevious !== undefined && receipt?.previous_receipt_hash !== expectedPrevious) {
|
|
264
|
+
errors.push(`receipt ${index}: previous hash mismatch`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const hash = safeReceiptHash(receipt);
|
|
268
|
+
hashes.push(hash);
|
|
269
|
+
expectedPrevious = hash;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const receiptLogRoot = new MerkleSet(hashes).root();
|
|
273
|
+
const expectedRoot = options.expectedReceiptLogRoot ?? options.receiptLogRoot ?? options.receipt_log_root;
|
|
274
|
+
if (expectedRoot !== undefined && normalizeSha256Root(expectedRoot, 'expectedReceiptLogRoot') !== receiptLogRoot) {
|
|
275
|
+
errors.push('receipt log root mismatch');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (options.verifyEmbeddedReceiptLogRoot === true) {
|
|
279
|
+
const prefix = new MerkleSet();
|
|
280
|
+
const startingRoot = options.previousReceiptLogRoot ?? options.previous_receipt_log_root ?? options.startReceiptLogRoot ?? options.start_receipt_log_root;
|
|
281
|
+
if (startingRoot !== undefined) {
|
|
282
|
+
const normalizedStartingRoot = normalizeSha256Root(startingRoot, 'previousReceiptLogRoot');
|
|
283
|
+
if (normalizedStartingRoot !== prefix.root()) errors.push('embedded receipt_log_root start root mismatch');
|
|
284
|
+
}
|
|
285
|
+
for (let index = 0; index < receipts.length; index += 1) {
|
|
286
|
+
const priorRoot = prefix.root();
|
|
287
|
+
if (receipts[index]?.receipt_log_root !== priorRoot) {
|
|
288
|
+
errors.push(`receipt ${index}: embedded receipt_log_root mismatch`);
|
|
289
|
+
}
|
|
290
|
+
prefix.insert(hashes[index]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const activeSetRoot = receipts.length > 0 ? receipts[receipts.length - 1].active_set_root : EMPTY_MERKLE_ROOT;
|
|
295
|
+
const expectedActiveRoot = options.expectedActiveSetRoot ?? options.activeSetRoot ?? options.active_set_root;
|
|
296
|
+
if (expectedActiveRoot !== undefined && normalizeSha256Root(expectedActiveRoot, 'expectedActiveSetRoot') !== activeSetRoot) {
|
|
297
|
+
errors.push('active set root mismatch');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (Array.isArray(options.expectedReceiptHashes)) {
|
|
301
|
+
if (options.expectedReceiptHashes.length !== hashes.length) {
|
|
302
|
+
errors.push('receipt hash list length mismatch');
|
|
303
|
+
}
|
|
304
|
+
const length = Math.min(options.expectedReceiptHashes.length, hashes.length);
|
|
305
|
+
for (let index = 0; index < length; index += 1) {
|
|
306
|
+
if (options.expectedReceiptHashes[index] !== hashes[index]) errors.push(`receipt ${index}: receipt hash mismatch`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return verificationResult(errors.length === 0, errors, {
|
|
311
|
+
receipt_log_root: receiptLogRoot,
|
|
312
|
+
active_set_root: activeSetRoot,
|
|
313
|
+
receipt_hashes: hashes
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export class MerkleSet {
|
|
318
|
+
constructor(values = []) {
|
|
319
|
+
this._values = new Set();
|
|
320
|
+
for (const value of values) this.insert(value);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
insert(value) {
|
|
324
|
+
this._values.add(normalizeLeafValue(value));
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
delete(value) {
|
|
329
|
+
return this._values.delete(normalizeLeafValue(value));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
has(value) {
|
|
333
|
+
return this._values.has(normalizeLeafValue(value));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
root() {
|
|
337
|
+
return merkleRoot(this._sortedValues());
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
values() {
|
|
341
|
+
return this._sortedValues();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
proveMembership(value) {
|
|
345
|
+
const leaf = normalizeLeafValue(value);
|
|
346
|
+
const values = this._sortedValues();
|
|
347
|
+
const index = values.indexOf(leaf);
|
|
348
|
+
if (index === -1) throw new RangeError('value is not a member of the MerkleSet');
|
|
349
|
+
return membershipProof(values, index);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
verifyMembership(proof, expectedRoot = proof?.root) {
|
|
353
|
+
return MerkleSet.verifyMembership(proof, expectedRoot);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
proveNonMembership(value) {
|
|
357
|
+
const leaf = normalizeLeafValue(value);
|
|
358
|
+
const values = this._sortedValues();
|
|
359
|
+
if (values.includes(leaf)) throw new RangeError('value is already a member of the MerkleSet');
|
|
360
|
+
const insertion = lowerBound(values, leaf);
|
|
361
|
+
const predecessor = insertion > 0 ? membershipProof(values, insertion - 1) : null;
|
|
362
|
+
const successor = insertion < values.length ? membershipProof(values, insertion) : null;
|
|
363
|
+
return {
|
|
364
|
+
type: 'non_membership',
|
|
365
|
+
value: leaf,
|
|
366
|
+
size: values.length,
|
|
367
|
+
root: merkleRoot(values),
|
|
368
|
+
predecessor,
|
|
369
|
+
successor
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
verifyNonMembership(proof, expectedRoot = proof?.root) {
|
|
374
|
+
return MerkleSet.verifyNonMembership(proof, expectedRoot);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
static verifyMembership(proof, expectedRoot = proof?.root) {
|
|
378
|
+
if (!isPlainRecord(proof) || proof.type !== 'membership' || typeof proof.value !== 'string') return false;
|
|
379
|
+
if (!Number.isInteger(proof.index) || proof.index < 0) return false;
|
|
380
|
+
if (!Number.isInteger(proof.size) || proof.size <= proof.index) return false;
|
|
381
|
+
if (!Array.isArray(proof.siblings)) return false;
|
|
382
|
+
let current = merkleLeafHash(proof.value);
|
|
383
|
+
for (const sibling of proof.siblings) {
|
|
384
|
+
if (!isPlainRecord(sibling) || !HASH_RE.test(sibling.hash ?? '')) return false;
|
|
385
|
+
if (sibling.position === 'left') current = merkleNodeHash(sibling.hash, current);
|
|
386
|
+
else if (sibling.position === 'right') current = merkleNodeHash(current, sibling.hash);
|
|
387
|
+
else return false;
|
|
388
|
+
}
|
|
389
|
+
const root = `${SHA256_PREFIX}${current}`;
|
|
390
|
+
return root === proof.root && root === expectedRoot;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
static verifyNonMembership(proof, expectedRoot = proof?.root) {
|
|
394
|
+
if (!isPlainRecord(proof) || proof.type !== 'non_membership' || typeof proof.value !== 'string') return false;
|
|
395
|
+
if (!Number.isInteger(proof.size) || proof.size < 0) return false;
|
|
396
|
+
if (proof.root !== expectedRoot) return false;
|
|
397
|
+
if (proof.size === 0) return proof.predecessor === null && proof.successor === null && proof.root === EMPTY_MERKLE_ROOT;
|
|
398
|
+
|
|
399
|
+
const predecessor = proof.predecessor;
|
|
400
|
+
const successor = proof.successor;
|
|
401
|
+
if (predecessor === null && successor === null) return false;
|
|
402
|
+
|
|
403
|
+
if (predecessor !== null) {
|
|
404
|
+
if (!MerkleSet.verifyMembership(predecessor, proof.root)) return false;
|
|
405
|
+
if (!(predecessor.value < proof.value)) return false;
|
|
406
|
+
}
|
|
407
|
+
if (successor !== null) {
|
|
408
|
+
if (!MerkleSet.verifyMembership(successor, proof.root)) return false;
|
|
409
|
+
if (!(proof.value < successor.value)) return false;
|
|
410
|
+
}
|
|
411
|
+
if (predecessor !== null && successor !== null) return predecessor.index + 1 === successor.index && predecessor.size === proof.size && successor.size === proof.size;
|
|
412
|
+
if (predecessor === null) return successor.index === 0 && successor.size === proof.size;
|
|
413
|
+
return predecessor.index === proof.size - 1 && predecessor.size === proof.size;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_sortedValues() {
|
|
417
|
+
return Array.from(this._values).sort();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function createCheckpoint(checkpointOrArgs = {}, maybeOptions = {}) {
|
|
422
|
+
const options = { ...(isPlainRecord(checkpointOrArgs) ? checkpointOrArgs : {}), ...maybeOptions };
|
|
423
|
+
const signer = normalizeSigner(options.signer, options.key_id ?? options.keyId, options.privateKey);
|
|
424
|
+
const body = {
|
|
425
|
+
schema: CHECKPOINT_SCHEMA,
|
|
426
|
+
tenant_hash: rootFromHashable(options.tenant_hash ?? options.tenantHash, options.tenant_id ?? options.tenantId ?? 'tenant'),
|
|
427
|
+
owner_scope_hash: rootFromHashable(options.owner_scope_hash ?? options.ownerScopeHash, options.owner_scope ?? options.ownerScope ?? 'owner'),
|
|
428
|
+
epoch: requiredInteger(options.epoch ?? options.sequence ?? 0, 'epoch'),
|
|
429
|
+
active_memory_root: normalizeSha256Root(options.active_memory_root ?? options.activeMemoryRoot ?? options.active_set_root ?? options.activeSetRoot ?? EMPTY_MERKLE_ROOT, 'active_memory_root'),
|
|
430
|
+
deleted_memory_root: normalizeSha256Root(options.deleted_memory_root ?? options.deletedMemoryRoot ?? EMPTY_MERKLE_ROOT, 'deleted_memory_root'),
|
|
431
|
+
receipt_log_root: normalizeSha256Root(options.receipt_log_root ?? options.receiptLogRoot ?? EMPTY_MERKLE_ROOT, 'receipt_log_root'),
|
|
432
|
+
key_registry_root: normalizeSha256Root(options.key_registry_root ?? options.keyRegistryRoot ?? EMPTY_MERKLE_ROOT, 'key_registry_root'),
|
|
433
|
+
policy_registry_root: normalizeSha256Root(options.policy_registry_root ?? options.policyRegistryRoot ?? EMPTY_MERKLE_ROOT, 'policy_registry_root'),
|
|
434
|
+
previous_checkpoint_hash: normalizePreviousHash(options.previous_checkpoint_hash ?? options.previousCheckpointHash ?? GENESIS),
|
|
435
|
+
issued_at: requiredString(options.issued_at ?? options.issuedAt ?? options.timestamp ?? new Date().toISOString(), 'issued_at')
|
|
436
|
+
};
|
|
437
|
+
if (options.witness_policy ?? options.witnessPolicy) body.witness_policy = options.witness_policy ?? options.witnessPolicy;
|
|
438
|
+
|
|
439
|
+
const checkpoint_id = options.checkpoint_id ?? options.checkpointId ?? `chk_${sha256Hex(canonicalize(body)).slice(0, 32)}`;
|
|
440
|
+
const unsigned = orderCheckpointFields({ ...body, checkpoint_id });
|
|
441
|
+
const signatures = options.signatures ?? [{
|
|
442
|
+
alg: ED25519,
|
|
443
|
+
key_id: signer.key_id,
|
|
444
|
+
value: signPayload({ payload: unsigned, privateKey: options.privateKey })
|
|
445
|
+
}];
|
|
446
|
+
return orderCheckpointFields({ ...unsigned, signatures });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function verifyCheckpoint(checkpointOrArgs, maybePublicKeys, maybeOptions = {}) {
|
|
450
|
+
const options = isPlainRecord(checkpointOrArgs) && Object.prototype.hasOwnProperty.call(checkpointOrArgs, 'checkpoint')
|
|
451
|
+
? checkpointOrArgs
|
|
452
|
+
: { checkpoint: checkpointOrArgs, publicKeys: maybePublicKeys, ...maybeOptions };
|
|
453
|
+
const checkpoint = options.checkpoint;
|
|
454
|
+
const errors = [];
|
|
455
|
+
|
|
456
|
+
if (!isPlainRecord(checkpoint)) return verificationResult(false, ['checkpoint must be an object']);
|
|
457
|
+
if (checkpoint.schema !== CHECKPOINT_SCHEMA) errors.push('schema mismatch');
|
|
458
|
+
for (const field of ['tenant_hash', 'owner_scope_hash', 'active_memory_root', 'deleted_memory_root', 'receipt_log_root', 'key_registry_root', 'policy_registry_root']) {
|
|
459
|
+
if (!ROOT_RE.test(checkpoint[field] ?? '')) errors.push(`${field} must be a sha256 root`);
|
|
460
|
+
}
|
|
461
|
+
if (!isPreviousHash(checkpoint.previous_checkpoint_hash)) errors.push('previous checkpoint hash mismatch');
|
|
462
|
+
if (!Number.isInteger(checkpoint.epoch) || checkpoint.epoch < 0) errors.push('epoch must be a non-negative integer');
|
|
463
|
+
if (!Array.isArray(checkpoint.signatures) || checkpoint.signatures.length === 0) errors.push('signatures missing');
|
|
464
|
+
|
|
465
|
+
const unsigned = unsignedCheckpoint(checkpoint);
|
|
466
|
+
for (const signature of checkpoint.signatures ?? []) {
|
|
467
|
+
const publicKey = resolvePublicKey(signature.key_id, options);
|
|
468
|
+
if (signature.alg !== ED25519 || !signature.key_id || !signature.value) {
|
|
469
|
+
errors.push('checkpoint signature malformed');
|
|
470
|
+
} else if (publicKey === undefined) {
|
|
471
|
+
errors.push(`missing public key for ${signature.key_id}`);
|
|
472
|
+
} else if (!verifySignature({ payload: unsigned, signature, publicKey })) {
|
|
473
|
+
errors.push(`checkpoint signature mismatch for ${signature.key_id}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const expectedReceiptLogRoot = options.expectedReceiptLogRoot ?? options.receiptLogRoot;
|
|
478
|
+
if (expectedReceiptLogRoot !== undefined && checkpoint.receipt_log_root !== normalizeSha256Root(expectedReceiptLogRoot, 'expectedReceiptLogRoot')) {
|
|
479
|
+
errors.push('receipt log root mismatch');
|
|
480
|
+
}
|
|
481
|
+
const expectedActiveRoot = options.expectedActiveMemoryRoot ?? options.activeMemoryRoot ?? options.activeSetRoot;
|
|
482
|
+
if (expectedActiveRoot !== undefined && checkpoint.active_memory_root !== normalizeSha256Root(expectedActiveRoot, 'expectedActiveMemoryRoot')) {
|
|
483
|
+
errors.push('active memory root mismatch');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return verificationResult(errors.length === 0, errors, { checkpoint_hash: prefixedSha256(canonicalize(checkpoint)) });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function createMemoryAddress(args = {}, maybeValue, maybeOptions = {}) {
|
|
490
|
+
const options = isPlainRecord(args) && (Object.prototype.hasOwnProperty.call(args, 'secret') || Object.prototype.hasOwnProperty.call(args, 'key'))
|
|
491
|
+
? args
|
|
492
|
+
: { secret: args, value: maybeValue, ...maybeOptions };
|
|
493
|
+
const secret = options.secret ?? options.key;
|
|
494
|
+
if (secret === undefined) throw new TypeError('createMemoryAddress requires a secret key');
|
|
495
|
+
const payload = {
|
|
496
|
+
namespace: options.namespace ?? 'memory',
|
|
497
|
+
tenant_id: options.tenant_id ?? options.tenantId ?? null,
|
|
498
|
+
subject_id: options.subject_id ?? options.subjectId ?? null,
|
|
499
|
+
value: options.value ?? options.plaintext ?? options.content ?? options.components ?? null
|
|
500
|
+
};
|
|
501
|
+
const digest = hmacSha256Hex({ key: secret, value: payload });
|
|
502
|
+
const prefix = options.prefix ?? 'mem';
|
|
503
|
+
return `${prefix}:hmac-sha256:${digest}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function bytesForHash(value) {
|
|
507
|
+
if (typeof value === 'string') return value;
|
|
508
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
|
|
509
|
+
return canonicalize(value);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function bytesForKey(value) {
|
|
513
|
+
if (typeof value === 'string' || Buffer.isBuffer(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
|
|
514
|
+
return canonicalize(value);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function bytesForSigning(payload) {
|
|
518
|
+
return Buffer.from(typeof payload === 'string' ? payload : canonicalize(payload));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function decodeSignature(signature) {
|
|
522
|
+
const value = isPlainRecord(signature) ? signature.value : signature;
|
|
523
|
+
if (typeof value !== 'string') throw new TypeError('signature must be a string');
|
|
524
|
+
return Buffer.from(value, value.includes('+') || value.includes('/') ? 'base64' : 'base64url');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function prefixedSha256(value) {
|
|
528
|
+
return `${SHA256_PREFIX}${sha256Hex(value)}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function normalizeSha256Root(value, field) {
|
|
532
|
+
if (typeof value !== 'string') throw new TypeError(`${field} must be a sha256 root`);
|
|
533
|
+
if (ROOT_RE.test(value)) return value;
|
|
534
|
+
if (HASH_RE.test(value)) return `${SHA256_PREFIX}${value}`;
|
|
535
|
+
throw new TypeError(`${field} must be a sha256 root`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function normalizePreviousHash(value) {
|
|
539
|
+
if (value === GENESIS) return GENESIS;
|
|
540
|
+
return normalizeSha256Root(value, 'previous hash');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function isPreviousHash(value) {
|
|
544
|
+
return value === GENESIS || ROOT_RE.test(value ?? '');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function normalizeSigner(signer, keyId, privateKey) {
|
|
548
|
+
if (isPlainRecord(signer)) {
|
|
549
|
+
return { key_id: requiredString(signer.key_id ?? signer.keyId, 'signer.key_id'), alg: signer.alg ?? ED25519 };
|
|
550
|
+
}
|
|
551
|
+
if (keyId !== undefined) return { key_id: requiredString(keyId, 'signer.key_id'), alg: ED25519 };
|
|
552
|
+
if (privateKey !== undefined) {
|
|
553
|
+
const publicKey = createPublicKey(privateKey).export({ type: 'spki', format: 'pem' });
|
|
554
|
+
return { key_id: `ed25519:${sha256Hex(publicKey).slice(0, 32)}`, alg: ED25519 };
|
|
555
|
+
}
|
|
556
|
+
throw new TypeError('signer or key_id is required');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function normalizeReceiptSignature(signature) {
|
|
560
|
+
if (!isPlainRecord(signature)) throw new TypeError('signature must be an object');
|
|
561
|
+
return { alg: signature.alg ?? ED25519, value: requiredString(signature.value, 'signature.value') };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function normalizeCreateReceiptArgs(receiptOrArgs, maybeOptions) {
|
|
565
|
+
if (isPlainRecord(receiptOrArgs) && (Object.prototype.hasOwnProperty.call(receiptOrArgs, 'event') || Object.prototype.hasOwnProperty.call(receiptOrArgs, 'privateKey'))) {
|
|
566
|
+
return receiptOrArgs;
|
|
567
|
+
}
|
|
568
|
+
return { ...maybeOptions, event: receiptOrArgs };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function normalizeVerifyReceiptArgs(receiptOrArgs, maybePublicKey, maybeOptions) {
|
|
572
|
+
if (isPlainRecord(receiptOrArgs) && Object.prototype.hasOwnProperty.call(receiptOrArgs, 'receipt')) {
|
|
573
|
+
return { receipt: receiptOrArgs.receipt, options: receiptOrArgs };
|
|
574
|
+
}
|
|
575
|
+
return { receipt: receiptOrArgs, options: { publicKey: maybePublicKey, ...maybeOptions } };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function eventHash(event) {
|
|
579
|
+
const safeEvent = { ...event };
|
|
580
|
+
delete safeEvent.privateKey;
|
|
581
|
+
delete safeEvent.signer;
|
|
582
|
+
delete safeEvent.signature;
|
|
583
|
+
return prefixedSha256(canonicalize(safeEvent));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function copyOptional(target, options, source, snake, camel) {
|
|
587
|
+
const value = options[snake] ?? options[camel] ?? source[snake] ?? source[camel];
|
|
588
|
+
if (value !== undefined) target[snake] = value;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function orderReceiptFields(receipt) {
|
|
592
|
+
const ordered = {};
|
|
593
|
+
for (const field of [
|
|
594
|
+
'schema',
|
|
595
|
+
'receipt_id',
|
|
596
|
+
'event_hash',
|
|
597
|
+
'operation',
|
|
598
|
+
'tenant_id',
|
|
599
|
+
'subject_id',
|
|
600
|
+
'memory_addr',
|
|
601
|
+
'source_addr',
|
|
602
|
+
'provider',
|
|
603
|
+
'model',
|
|
604
|
+
'policy_id',
|
|
605
|
+
'sequence',
|
|
606
|
+
'previous_receipt_hash',
|
|
607
|
+
'active_set_root',
|
|
608
|
+
'receipt_log_root',
|
|
609
|
+
'timestamp',
|
|
610
|
+
'signer',
|
|
611
|
+
'signature'
|
|
612
|
+
]) {
|
|
613
|
+
if (receipt[field] !== undefined) ordered[field] = receipt[field];
|
|
614
|
+
}
|
|
615
|
+
return ordered;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function unsignedReceipt(receipt) {
|
|
619
|
+
const { signature, ...unsigned } = receipt;
|
|
620
|
+
return orderReceiptFields(unsigned);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function validateReceiptShape(receipt, errors) {
|
|
624
|
+
for (const key of Object.keys(receipt)) {
|
|
625
|
+
if (!RECEIPT_TOP_LEVEL_FIELDS.has(key)) errors.push(`unknown receipt field: ${key}`);
|
|
626
|
+
if (RECEIPT_PUBLIC_PLAINTEXT_KEYS.has(key)) errors.push(`receipt must not contain public plaintext field: ${key}`);
|
|
627
|
+
}
|
|
628
|
+
if (isPlainRecord(receipt.signature)) {
|
|
629
|
+
for (const key of Object.keys(receipt.signature)) {
|
|
630
|
+
if (!RECEIPT_SIGNATURE_FIELDS.has(key)) errors.push(`unknown signature field: ${key}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function deriveReceiptId(receipt) {
|
|
636
|
+
const { receipt_id, signature, ...body } = receipt;
|
|
637
|
+
return `rcpt_${sha256Hex(canonicalize(orderReceiptFields({ ...withoutUndefinedFields(body), receipt_id: '' }))).slice(0, 32)}`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function safeReceiptHash(receipt) {
|
|
641
|
+
try {
|
|
642
|
+
return receiptHash(receipt);
|
|
643
|
+
} catch {
|
|
644
|
+
return `${SHA256_PREFIX}${'0'.repeat(64)}`;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function merkleRoot(values) {
|
|
649
|
+
if (values.length === 0) return EMPTY_MERKLE_ROOT;
|
|
650
|
+
let level = values.map(merkleLeafHash);
|
|
651
|
+
while (level.length > 1) level = nextMerkleLevel(level);
|
|
652
|
+
return `${SHA256_PREFIX}${level[0]}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function nextMerkleLevel(level) {
|
|
656
|
+
const next = [];
|
|
657
|
+
for (let index = 0; index < level.length; index += 2) {
|
|
658
|
+
next.push(index + 1 < level.length ? merkleNodeHash(level[index], level[index + 1]) : level[index]);
|
|
659
|
+
}
|
|
660
|
+
return next;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function merkleLeafHash(value) {
|
|
664
|
+
return sha256Hex(`enigma.merkle.leaf.v1\0${value}`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function merkleNodeHash(left, right) {
|
|
668
|
+
return sha256Hex(`enigma.merkle.node.v1\0${left}\0${right}`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function membershipProof(values, index) {
|
|
672
|
+
const value = values[index];
|
|
673
|
+
const siblings = [];
|
|
674
|
+
let level = values.map(merkleLeafHash);
|
|
675
|
+
let cursor = index;
|
|
676
|
+
while (level.length > 1) {
|
|
677
|
+
const siblingIndex = cursor % 2 === 0 ? cursor + 1 : cursor - 1;
|
|
678
|
+
if (siblingIndex < level.length) {
|
|
679
|
+
siblings.push({ position: siblingIndex < cursor ? 'left' : 'right', hash: level[siblingIndex] });
|
|
680
|
+
}
|
|
681
|
+
level = nextMerkleLevel(level);
|
|
682
|
+
cursor = Math.floor(cursor / 2);
|
|
683
|
+
}
|
|
684
|
+
return { type: 'membership', value, index, size: values.length, root: merkleRoot(values), siblings };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function lowerBound(values, value) {
|
|
688
|
+
let low = 0;
|
|
689
|
+
let high = values.length;
|
|
690
|
+
while (low < high) {
|
|
691
|
+
const mid = (low + high) >> 1;
|
|
692
|
+
if (values[mid] < value) low = mid + 1;
|
|
693
|
+
else high = mid;
|
|
694
|
+
}
|
|
695
|
+
return low;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function normalizeLeafValue(value) {
|
|
699
|
+
return typeof value === 'string' ? value : canonicalize(value);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function rootFromHashable(root, fallbackValue) {
|
|
703
|
+
if (root !== undefined) return normalizeSha256Root(root, 'root');
|
|
704
|
+
return prefixedSha256(canonicalize(fallbackValue));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function orderCheckpointFields(checkpoint) {
|
|
708
|
+
const ordered = {
|
|
709
|
+
schema: checkpoint.schema,
|
|
710
|
+
checkpoint_id: checkpoint.checkpoint_id,
|
|
711
|
+
tenant_hash: checkpoint.tenant_hash,
|
|
712
|
+
owner_scope_hash: checkpoint.owner_scope_hash,
|
|
713
|
+
epoch: checkpoint.epoch,
|
|
714
|
+
active_memory_root: checkpoint.active_memory_root,
|
|
715
|
+
deleted_memory_root: checkpoint.deleted_memory_root,
|
|
716
|
+
receipt_log_root: checkpoint.receipt_log_root,
|
|
717
|
+
key_registry_root: checkpoint.key_registry_root,
|
|
718
|
+
policy_registry_root: checkpoint.policy_registry_root,
|
|
719
|
+
previous_checkpoint_hash: checkpoint.previous_checkpoint_hash
|
|
720
|
+
};
|
|
721
|
+
if (checkpoint.witness_policy !== undefined) ordered.witness_policy = checkpoint.witness_policy;
|
|
722
|
+
ordered.issued_at = checkpoint.issued_at;
|
|
723
|
+
if (checkpoint.signatures !== undefined) ordered.signatures = checkpoint.signatures;
|
|
724
|
+
return ordered;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function unsignedCheckpoint(checkpoint) {
|
|
728
|
+
const { signatures, ...unsigned } = checkpoint;
|
|
729
|
+
return orderCheckpointFields(unsigned);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function resolvePublicKey(keyId, options) {
|
|
733
|
+
if (options.publicKey !== undefined) return options.publicKey;
|
|
734
|
+
const publicKeys = options.publicKeys ?? options.keyring ?? options.keys;
|
|
735
|
+
if (publicKeys === undefined || keyId === undefined) return undefined;
|
|
736
|
+
if (publicKeys instanceof Map) return publicKeys.get(keyId);
|
|
737
|
+
if (typeof publicKeys === 'function') return publicKeys(keyId);
|
|
738
|
+
if (isPlainRecord(publicKeys)) return publicKeys[keyId];
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function verificationResult(ok, errors, extra = {}) {
|
|
743
|
+
return { ok, valid: ok, errors, ...extra };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function requiredString(value, field) {
|
|
747
|
+
if (typeof value !== 'string' || value.length === 0) throw new TypeError(`${field} is required`);
|
|
748
|
+
return value;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function requiredInteger(value, field) {
|
|
752
|
+
if (!Number.isInteger(value) || value < 0) throw new TypeError(`${field} must be a non-negative integer`);
|
|
753
|
+
return value;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function withoutUndefinedFields(record) {
|
|
757
|
+
const cleaned = {};
|
|
758
|
+
for (const key of Object.keys(record)) {
|
|
759
|
+
if (record[key] !== undefined) cleaned[key] = record[key];
|
|
760
|
+
}
|
|
761
|
+
return cleaned;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function isPlainRecord(value) {
|
|
765
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
766
|
+
}
|