mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 15:50:25 +02:00
* test(memory): add cross-harness conformance example * fix: update js-yaml to patched 4.3.2 * feat(memory): verify recalled evidence against scoped source catalog * fix: preserve control-character validation under lint
80 lines
3.9 KiB
JavaScript
80 lines
3.9 KiB
JavaScript
'use strict';
|
|
|
|
// Example-only integrity checks. A host-owned catalog is not an identity provider.
|
|
const { createHash } = require('node:crypto');
|
|
const SCHEMA = 'ecc.memory.example-evidence.v1';
|
|
const MAX_BODY_BYTES = 16 * 1024;
|
|
const MAX_TEXT_BYTES = 8 * 1024;
|
|
const ENVELOPE_KEYS = ['schema', 'sourceRef', 'sha256', 'text', 'observedAt', 'sessionId', 'checkpointId'];
|
|
const SOURCE_KEYS = ['workspace', 'scope', 'text', 'observedAt', 'sessionId', 'checkpointId'];
|
|
const slug = value => typeof value === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(value);
|
|
const sourceRefIsValid = value => typeof value === 'string' && /^fixture:[a-z][a-z0-9-]{0,63}$/.test(value);
|
|
const digest = text => createHash('sha256').update(text, 'utf8').digest('hex');
|
|
|
|
function fail(code) {
|
|
const error = new Error(`Memory example evidence: ${code}`);
|
|
error.code = code;
|
|
throw error;
|
|
}
|
|
function hasExactKeys(value, keys) {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
&& Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
}
|
|
function validObservation(value) {
|
|
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return false;
|
|
const date = new Date(value);
|
|
return Number.isFinite(date.getTime()) && date.toISOString() === value;
|
|
}
|
|
function validSourceFields(value) {
|
|
return typeof value.text === 'string' && value.text.length > 0 && value.text.length <= MAX_TEXT_BYTES
|
|
&& Buffer.byteLength(value.text, 'utf8') <= MAX_TEXT_BYTES
|
|
// eslint-disable-next-line no-control-regex -- Intentionally reject C0 except tab/LF/CR, and DEL.
|
|
&& !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value.text)
|
|
&& validObservation(value.observedAt) && slug(value.sessionId) && slug(value.checkpointId);
|
|
}
|
|
function validateEnvelope(value) {
|
|
if (!hasExactKeys(value, ENVELOPE_KEYS) || value.schema !== SCHEMA || !sourceRefIsValid(value.sourceRef)
|
|
|| typeof value.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.sha256) || !validSourceFields(value)) {
|
|
fail('INVALID_ENVELOPE');
|
|
}
|
|
}
|
|
function getSource(sourceRef, catalog, context) {
|
|
if (!hasExactKeys(context, ['workspace', 'scope']) || !slug(context.workspace)
|
|
|| !['project', 'team', 'user'].includes(context.scope)) fail('INVALID_CONTEXT');
|
|
if (!(catalog instanceof Map) || !sourceRefIsValid(sourceRef)) fail('INVALID_SOURCE');
|
|
const source = catalog.get(sourceRef);
|
|
if (source === undefined) fail('SOURCE_UNAVAILABLE');
|
|
if (!hasExactKeys(source, SOURCE_KEYS) || !validSourceFields(source) || !slug(source.workspace)
|
|
|| !['project', 'team', 'user'].includes(source.scope)) fail('INVALID_SOURCE');
|
|
if (source.workspace !== context.workspace || source.scope !== context.scope) fail('CONTEXT_MISMATCH');
|
|
return source;
|
|
}
|
|
function decode(body) {
|
|
if (typeof body !== 'string' || body.length > MAX_BODY_BYTES || Buffer.byteLength(body, 'utf8') > MAX_BODY_BYTES) {
|
|
fail('INVALID_ENVELOPE');
|
|
}
|
|
let value;
|
|
try { value = JSON.parse(body); } catch { fail('INVALID_ENVELOPE'); }
|
|
validateEnvelope(value);
|
|
return value;
|
|
}
|
|
|
|
function encodeEvidence(sourceRef, catalog, context) {
|
|
const source = getSource(sourceRef, catalog, context);
|
|
const body = JSON.stringify({ schema: SCHEMA, sourceRef, sha256: digest(source.text), text: source.text,
|
|
observedAt: source.observedAt, sessionId: source.sessionId, checkpointId: source.checkpointId });
|
|
decode(body);
|
|
return body;
|
|
}
|
|
|
|
function verifyEvidence(body, catalog, context) {
|
|
const value = decode(body);
|
|
const source = getSource(value.sourceRef, catalog, context);
|
|
if (value.sha256 !== digest(source.text) || value.text !== source.text
|
|
|| value.observedAt !== source.observedAt || value.sessionId !== source.sessionId
|
|
|| value.checkpointId !== source.checkpointId) fail('SOURCE_MISMATCH');
|
|
return Object.freeze({ status: 'source-content-match', sourceRef: value.sourceRef, sha256: value.sha256 });
|
|
}
|
|
|
|
module.exports = { encodeEvidence, verifyEvidence };
|