359 lines
10 KiB
JavaScript
359 lines
10 KiB
JavaScript
import { createEnclave } from '../dist/enclave.js';
|
|
|
|
let enclave = null;
|
|
let helia = null;
|
|
let lastCID = null;
|
|
let lastDatabase = null;
|
|
|
|
const LogLevel = { INFO: 'info', OK: 'ok', ERR: 'err', DATA: 'data' };
|
|
|
|
function log(card, level, message, data = null) {
|
|
const el = document.getElementById(`log-${card}`);
|
|
if (!el) return console.log(`[${card}] ${message}`, data ?? '');
|
|
|
|
const time = new Date().toISOString().slice(11, 23);
|
|
|
|
let entry = `<div class="log-entry"><span class="log-time">${time}</span> <span class="log-${level}">${message}</span>`;
|
|
if (data !== null) {
|
|
const json = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
entry += `<span class="log-data">${json}</span>`;
|
|
}
|
|
entry += '</div>';
|
|
|
|
el.innerHTML += entry;
|
|
el.classList.add('has-content');
|
|
el.scrollTop = el.scrollHeight;
|
|
|
|
console.log(`[${time}] [${card}] ${message}`, data ?? '');
|
|
}
|
|
|
|
function setStatus(id, ok, message) {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.textContent = message;
|
|
el.className = `status ${ok ? 'ok' : ok === false ? 'err' : 'wait'}`;
|
|
}
|
|
}
|
|
|
|
function setCID(cid) {
|
|
const el = document.getElementById('cid-display');
|
|
const input = document.getElementById('cid-input');
|
|
if (el && cid) {
|
|
el.textContent = `CID: ${cid}`;
|
|
el.classList.add('visible');
|
|
}
|
|
if (input && cid) {
|
|
input.value = cid;
|
|
}
|
|
lastCID = cid;
|
|
}
|
|
|
|
function arrayBufferToBase64(buffer) {
|
|
const bytes = new Uint8Array(buffer);
|
|
let binary = '';
|
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
async function createWebAuthnCredential() {
|
|
const userId = crypto.getRandomValues(new Uint8Array(16));
|
|
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
|
|
const publicKeyCredentialCreationOptions = {
|
|
challenge,
|
|
rp: {
|
|
name: "Motr Enclave",
|
|
id: window.location.hostname,
|
|
},
|
|
user: {
|
|
id: userId,
|
|
name: `user-${Date.now()}@motr.local`,
|
|
displayName: "Motr User",
|
|
},
|
|
pubKeyCredParams: [
|
|
{ alg: -7, type: "public-key" },
|
|
{ alg: -257, type: "public-key" },
|
|
],
|
|
authenticatorSelection: {
|
|
authenticatorAttachment: "platform",
|
|
userVerification: "preferred",
|
|
residentKey: "preferred",
|
|
},
|
|
timeout: 60000,
|
|
attestation: "none",
|
|
};
|
|
|
|
const credential = await navigator.credentials.create({
|
|
publicKey: publicKeyCredentialCreationOptions,
|
|
});
|
|
|
|
return {
|
|
id: credential.id,
|
|
rawId: arrayBufferToBase64(credential.rawId),
|
|
type: credential.type,
|
|
response: {
|
|
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
|
|
attestationObject: arrayBufferToBase64(credential.response.attestationObject),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function initHelia() {
|
|
try {
|
|
setStatus('ipfs-status', null, 'IPFS...');
|
|
log('generate', LogLevel.INFO, 'Initializing IPFS (Helia)...');
|
|
|
|
const { createHelia } = await import('helia');
|
|
helia = await createHelia();
|
|
|
|
setStatus('ipfs-status', true, 'IPFS Ready');
|
|
log('generate', LogLevel.OK, 'IPFS initialized');
|
|
return helia;
|
|
} catch (err) {
|
|
setStatus('ipfs-status', false, 'IPFS Failed');
|
|
log('generate', LogLevel.ERR, `IPFS init failed: ${err?.message || String(err)}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
try {
|
|
log('generate', LogLevel.INFO, 'Loading enclave.wasm...');
|
|
|
|
await initHelia();
|
|
|
|
enclave = await createEnclave('./enclave.wasm', {
|
|
debug: true,
|
|
ipfs: helia
|
|
});
|
|
|
|
setStatus('status', true, 'Ready');
|
|
log('generate', LogLevel.OK, `Plugin loaded (IPFS: ${enclave.ipfsEnabled ? 'enabled' : 'disabled'})`);
|
|
} catch (err) {
|
|
setStatus('status', false, 'Failed');
|
|
log('generate', LogLevel.ERR, `Load failed: ${err?.message || String(err)}`);
|
|
}
|
|
}
|
|
|
|
window.testPing = async function() {
|
|
if (!enclave) return log('ping', LogLevel.ERR, 'Plugin not loaded');
|
|
|
|
const message = document.getElementById('ping-msg').value || 'hello';
|
|
log('ping', LogLevel.INFO, `Sending: "${message}"`);
|
|
|
|
try {
|
|
const result = await enclave.ping(message);
|
|
if (result.success) {
|
|
log('ping', LogLevel.OK, `Response: "${result.echo}"`, result);
|
|
} else {
|
|
log('ping', LogLevel.ERR, result.message, result);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('ping', LogLevel.ERR, err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testGenerate = async function() {
|
|
if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded');
|
|
|
|
if (!window.PublicKeyCredential) {
|
|
log('generate', LogLevel.ERR, 'WebAuthn not supported in this browser');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
log('generate', LogLevel.INFO, 'Requesting WebAuthn credential...');
|
|
|
|
const credential = await createWebAuthnCredential();
|
|
log('generate', LogLevel.OK, `Credential created: ${credential.id.slice(0, 20)}...`);
|
|
|
|
const credentialJson = JSON.stringify(credential);
|
|
const credentialBase64 = btoa(credentialJson);
|
|
|
|
log('generate', LogLevel.INFO, 'Calling enclave.generate()...');
|
|
const result = await enclave.generate(credentialBase64);
|
|
|
|
const logData = { did: result.did, dbSize: result.database?.length };
|
|
if (result.cid) {
|
|
logData.cid = result.cid;
|
|
setCID(result.cid);
|
|
log('generate', LogLevel.OK, `Stored to IPFS: ${result.cid}`);
|
|
}
|
|
|
|
log('generate', LogLevel.OK, `DID created: ${result.did}`, logData);
|
|
|
|
if (result.database) {
|
|
lastDatabase = result.database;
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
if (err.name === 'NotAllowedError') {
|
|
log('generate', LogLevel.ERR, 'User cancelled or WebAuthn not allowed');
|
|
} else {
|
|
log('generate', LogLevel.ERR, err?.message || String(err));
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testGenerateMock = async function() {
|
|
if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded');
|
|
|
|
const mockCredential = btoa(JSON.stringify({
|
|
id: `mock-${Date.now()}`,
|
|
rawId: arrayBufferToBase64(crypto.getRandomValues(new Uint8Array(32))),
|
|
type: 'public-key',
|
|
response: {
|
|
clientDataJSON: arrayBufferToBase64(new TextEncoder().encode('{"mock":true}')),
|
|
attestationObject: arrayBufferToBase64(crypto.getRandomValues(new Uint8Array(64))),
|
|
},
|
|
}));
|
|
|
|
log('generate', LogLevel.INFO, 'Using mock credential...');
|
|
|
|
try {
|
|
const result = await enclave.generate(mockCredential);
|
|
|
|
const logData = { did: result.did, dbSize: result.database?.length };
|
|
if (result.cid) {
|
|
logData.cid = result.cid;
|
|
setCID(result.cid);
|
|
log('generate', LogLevel.OK, `Stored to IPFS: ${result.cid}`);
|
|
}
|
|
|
|
log('generate', LogLevel.OK, `DID created: ${result.did}`, logData);
|
|
|
|
if (result.database) {
|
|
lastDatabase = result.database;
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('generate', LogLevel.ERR, err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testLoadFromCID = async function() {
|
|
if (!enclave) return log('load', LogLevel.ERR, 'Plugin not loaded');
|
|
|
|
const cid = document.getElementById('cid-input').value;
|
|
if (!cid) return log('load', LogLevel.ERR, 'No CID - run generate first');
|
|
|
|
if (!cid.startsWith('bafy')) {
|
|
log('load', LogLevel.ERR, 'Invalid CID format (should start with bafy...)');
|
|
return;
|
|
}
|
|
|
|
log('load', LogLevel.INFO, `Loading from IPFS: ${cid}`);
|
|
|
|
try {
|
|
const result = await enclave.load(cid);
|
|
|
|
if (result.success) {
|
|
log('load', LogLevel.OK, `Loaded DID: ${result.did}`, result);
|
|
} else {
|
|
log('load', LogLevel.ERR, result.error, result);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('load', LogLevel.ERR, err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testLoadFromBytes = async function() {
|
|
if (!enclave) return log('load', LogLevel.ERR, 'Plugin not loaded');
|
|
|
|
if (!lastDatabase) {
|
|
return log('load', LogLevel.ERR, 'No database in memory - run generate first');
|
|
}
|
|
|
|
log('load', LogLevel.INFO, `Loading from bytes (${lastDatabase.length} bytes)...`);
|
|
|
|
try {
|
|
const result = await enclave.load(new Uint8Array(lastDatabase));
|
|
|
|
if (result.success) {
|
|
log('load', LogLevel.OK, `Loaded DID: ${result.did}`, result);
|
|
} else {
|
|
log('load', LogLevel.ERR, result.error, result);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('load', LogLevel.ERR, err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testExec = async function() {
|
|
if (!enclave) return log('exec', LogLevel.ERR, 'Plugin not loaded');
|
|
|
|
const filter = document.getElementById('filter').value;
|
|
if (!filter) return log('exec', LogLevel.ERR, 'Filter required');
|
|
|
|
log('exec', LogLevel.INFO, `Executing: ${filter}`);
|
|
|
|
try {
|
|
const result = await enclave.exec(filter);
|
|
|
|
if (result.success) {
|
|
log('exec', LogLevel.OK, 'Success', result);
|
|
} else {
|
|
log('exec', LogLevel.ERR, result.error, result);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('exec', LogLevel.ERR, err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testQuery = async function() {
|
|
if (!enclave) return log('query', LogLevel.ERR, 'Plugin not loaded');
|
|
|
|
const did = document.getElementById('did').value;
|
|
log('query', LogLevel.INFO, did ? `Querying: ${did}` : 'Querying current DID...');
|
|
|
|
try {
|
|
const result = await enclave.query(did);
|
|
log('query', LogLevel.OK, `Resolved: ${result.did}`, result);
|
|
return result;
|
|
} catch (err) {
|
|
log('query', LogLevel.ERR, err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.setFilter = function(filter) {
|
|
document.getElementById('filter').value = filter;
|
|
};
|
|
|
|
window.clearCardLog = function(card) {
|
|
const el = document.getElementById(`log-${card}`);
|
|
if (el) {
|
|
el.innerHTML = '';
|
|
el.classList.remove('has-content');
|
|
}
|
|
};
|
|
|
|
window.runAllTests = async function() {
|
|
log('ping', LogLevel.INFO, '=== Running all tests ===');
|
|
|
|
try {
|
|
await testPing();
|
|
await testGenerateMock();
|
|
await testLoadFromCID();
|
|
await testExec();
|
|
await testQuery();
|
|
log('query', LogLevel.OK, '=== All tests passed ===');
|
|
} catch (err) {
|
|
log('query', LogLevel.ERR, `Tests failed: ${err?.message || String(err)}`);
|
|
}
|
|
};
|
|
|
|
init();
|