447 lines
13 KiB
JavaScript
447 lines
13 KiB
JavaScript
import { createEnclave } from '../dist/enclave.js';
|
|
|
|
let enclave = null;
|
|
let lastDatabase = null;
|
|
let currentResource = 'accounts';
|
|
let currentAction = 'list';
|
|
|
|
const RESOURCE_ACTIONS = {
|
|
accounts: ['list', 'get'],
|
|
enclaves: ['list', 'get', 'rotate', 'archive', 'delete'],
|
|
credentials: ['list', 'get'],
|
|
sessions: ['list', 'revoke'],
|
|
grants: ['list', 'revoke'],
|
|
delegations: ['list', 'list_received', 'list_command', 'get', 'revoke', 'verify', 'cleanup'],
|
|
ucans: ['list', 'get', 'revoke', 'verify', 'cleanup'],
|
|
verification_methods: ['list', 'get', 'delete'],
|
|
services: ['list', 'get', 'get_by_id'],
|
|
};
|
|
|
|
const ACTIONS_REQUIRING_SUBJECT = ['get', 'revoke', 'delete', 'verify', 'rotate', 'archive', 'list_received', 'list_command', 'get_by_id'];
|
|
|
|
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">${escapeHtml(json)}</span>`;
|
|
}
|
|
entry += '</div>';
|
|
|
|
el.innerHTML += entry;
|
|
el.classList.add('visible');
|
|
el.scrollTop = el.scrollHeight;
|
|
|
|
console.log(`[${time}] [${card}] ${message}`, data ?? '');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
function setStatus(ready, message) {
|
|
const dot = document.getElementById('status-dot');
|
|
const text = document.getElementById('status-text');
|
|
|
|
if (dot) {
|
|
dot.className = 'status-dot' + (ready === true ? ' ready' : ready === false ? ' error' : '');
|
|
}
|
|
if (text) {
|
|
text.textContent = message;
|
|
}
|
|
}
|
|
|
|
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 options = {
|
|
challenge,
|
|
rp: {
|
|
name: "Motr Enclave Demo",
|
|
id: window.location.hostname,
|
|
},
|
|
user: {
|
|
id: userId,
|
|
name: `user-${Date.now()}@motr.local`,
|
|
displayName: "Motr Demo 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: options });
|
|
|
|
return {
|
|
id: credential.id,
|
|
rawId: arrayBufferToBase64(credential.rawId),
|
|
type: credential.type,
|
|
response: {
|
|
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
|
|
attestationObject: arrayBufferToBase64(credential.response.attestationObject),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function init() {
|
|
try {
|
|
setStatus(null, 'Loading...');
|
|
enclave = await createEnclave('./enclave.wasm', { debug: true });
|
|
setStatus(true, 'Ready');
|
|
log('generate', 'ok', 'Plugin loaded successfully');
|
|
} catch (err) {
|
|
setStatus(false, 'Failed');
|
|
log('generate', 'err', `Load failed: ${err?.message || String(err)}`);
|
|
}
|
|
}
|
|
|
|
window.showSection = function(section) {
|
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
|
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
|
|
|
|
const sectionEl = document.getElementById(`section-${section}`);
|
|
if (sectionEl) sectionEl.classList.add('active');
|
|
|
|
event?.target?.classList.add('active');
|
|
};
|
|
|
|
window.testPing = async function() {
|
|
if (!enclave) return log('ping', 'err', 'Plugin not loaded');
|
|
|
|
const message = document.getElementById('ping-msg')?.value || 'hello';
|
|
log('ping', 'info', `Sending: "${message}"`);
|
|
|
|
try {
|
|
const result = await enclave.ping(message);
|
|
if (result.success) {
|
|
log('ping', 'ok', `Response: "${result.echo}"`, result);
|
|
} else {
|
|
log('ping', 'err', result.message, result);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('ping', 'err', err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testGenerate = async function() {
|
|
if (!enclave) return log('generate', 'err', 'Plugin not loaded');
|
|
|
|
if (!window.PublicKeyCredential) {
|
|
log('generate', 'err', 'WebAuthn not supported in this browser');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
log('generate', 'info', 'Requesting WebAuthn credential...');
|
|
const credential = await createWebAuthnCredential();
|
|
log('generate', 'ok', `Credential created: ${credential.id.slice(0, 20)}...`);
|
|
|
|
const credentialBase64 = btoa(JSON.stringify(credential));
|
|
|
|
log('generate', 'info', 'Calling enclave.generate()...');
|
|
const result = await enclave.generate(credentialBase64);
|
|
|
|
log('generate', 'ok', `DID created: ${result.did}`, {
|
|
did: result.did,
|
|
enclaveId: result.enclave_id,
|
|
publicKey: result.public_key?.slice(0, 20) + '...',
|
|
accounts: result.accounts?.length ?? 0,
|
|
dbSize: result.database?.length ?? 0,
|
|
});
|
|
|
|
if (result.database) {
|
|
lastDatabase = result.database;
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
if (err.name === 'NotAllowedError') {
|
|
log('generate', 'err', 'User cancelled or WebAuthn not allowed');
|
|
} else {
|
|
log('generate', 'err', err?.message || String(err));
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testGenerateMock = async function() {
|
|
if (!enclave) return log('generate', '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', 'info', 'Using mock credential...');
|
|
|
|
try {
|
|
const result = await enclave.generate(mockCredential);
|
|
|
|
log('generate', 'ok', `DID created: ${result.did}`, {
|
|
did: result.did,
|
|
enclaveId: result.enclave_id,
|
|
publicKey: result.public_key?.slice(0, 20) + '...',
|
|
accounts: result.accounts?.length ?? 0,
|
|
dbSize: result.database?.length ?? 0,
|
|
});
|
|
|
|
if (result.database) {
|
|
lastDatabase = result.database;
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('generate', 'err', err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.testLoadFromBytes = async function() {
|
|
if (!enclave) return log('load', 'err', 'Plugin not loaded');
|
|
|
|
if (!lastDatabase) {
|
|
return log('load', 'err', 'No database in memory - run generate first');
|
|
}
|
|
|
|
log('load', 'info', `Loading from bytes (${lastDatabase.length} bytes)...`);
|
|
|
|
try {
|
|
const result = await enclave.load(new Uint8Array(lastDatabase));
|
|
|
|
if (result.success) {
|
|
log('load', 'ok', `Loaded DID: ${result.did}`, result);
|
|
} else {
|
|
log('load', 'err', result.error, result);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
log('load', 'err', err?.message || String(err));
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
window.downloadDatabase = function() {
|
|
if (!lastDatabase) {
|
|
alert('No database in memory. Run generate first.');
|
|
return;
|
|
}
|
|
|
|
const blob = new Blob([new Uint8Array(lastDatabase)], { type: 'application/octet-stream' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `enclave-${Date.now()}.db`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
|
|
log('load', 'ok', `Downloaded database (${lastDatabase.length} bytes)`);
|
|
};
|
|
|
|
window.uploadDatabase = async function(event) {
|
|
const file = event.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
const buffer = await file.arrayBuffer();
|
|
lastDatabase = Array.from(new Uint8Array(buffer));
|
|
|
|
log('load', 'info', `Uploaded ${file.name} (${lastDatabase.length} bytes)`);
|
|
|
|
if (enclave) {
|
|
await testLoadFromBytes();
|
|
}
|
|
};
|
|
|
|
window.selectResource = function(resource) {
|
|
currentResource = resource;
|
|
currentAction = 'list';
|
|
|
|
document.querySelectorAll('.resource-btn').forEach(el => el.classList.remove('active'));
|
|
event?.target?.closest('.resource-btn')?.classList.add('active');
|
|
|
|
document.getElementById('selected-resource').textContent = resource;
|
|
|
|
const actions = RESOURCE_ACTIONS[resource] || ['list'];
|
|
const pillsContainer = document.getElementById('action-pills');
|
|
pillsContainer.innerHTML = actions.map((action, i) =>
|
|
`<button class="action-pill${i === 0 ? ' active' : ''}" onclick="selectAction('${action}')">${action}</button>`
|
|
).join('');
|
|
|
|
updateSubjectRow();
|
|
};
|
|
|
|
window.selectAction = function(action) {
|
|
currentAction = action;
|
|
|
|
document.querySelectorAll('.action-pill').forEach(el => el.classList.remove('active'));
|
|
event?.target?.classList.add('active');
|
|
|
|
updateSubjectRow();
|
|
};
|
|
|
|
function updateSubjectRow() {
|
|
const subjectRow = document.getElementById('subject-row');
|
|
const subjectInput = document.getElementById('subject-input');
|
|
|
|
if (ACTIONS_REQUIRING_SUBJECT.includes(currentAction)) {
|
|
subjectRow.style.display = 'block';
|
|
|
|
const placeholders = {
|
|
get: 'ID or address',
|
|
revoke: 'ID to revoke',
|
|
delete: 'ID to delete',
|
|
verify: 'CID to verify',
|
|
rotate: 'Enclave ID',
|
|
archive: 'Enclave ID',
|
|
list_received: 'Audience DID',
|
|
list_command: 'Command (e.g., msg/send)',
|
|
get_by_id: 'Service ID (number)',
|
|
};
|
|
|
|
subjectInput.placeholder = placeholders[currentAction] || 'Subject';
|
|
} else {
|
|
subjectRow.style.display = 'none';
|
|
subjectInput.value = '';
|
|
}
|
|
}
|
|
|
|
window.executeAction = async function() {
|
|
if (!enclave) {
|
|
showResult('Plugin not loaded', true);
|
|
return;
|
|
}
|
|
|
|
const subject = document.getElementById('subject-input')?.value || '';
|
|
|
|
if (ACTIONS_REQUIRING_SUBJECT.includes(currentAction) && !subject) {
|
|
showResult(`Subject is required for action: ${currentAction}`, true);
|
|
return;
|
|
}
|
|
|
|
let filter = `resource:${currentResource} action:${currentAction}`;
|
|
if (subject) {
|
|
filter += ` subject:${subject}`;
|
|
}
|
|
|
|
try {
|
|
const result = await enclave.exec(filter);
|
|
|
|
if (result.success) {
|
|
showResult(JSON.stringify(result.result, null, 2), false);
|
|
} else {
|
|
showResult(result.error, true);
|
|
}
|
|
} catch (err) {
|
|
showResult(err?.message || String(err), true);
|
|
}
|
|
};
|
|
|
|
window.quickExec = async function(resource, action) {
|
|
if (!enclave) {
|
|
alert('Plugin not loaded');
|
|
return;
|
|
}
|
|
|
|
showSection('explorer');
|
|
selectResource(resource);
|
|
selectAction(action);
|
|
await executeAction();
|
|
};
|
|
|
|
function showResult(content, isError = false) {
|
|
const resultEl = document.getElementById('exec-result');
|
|
resultEl.textContent = content;
|
|
resultEl.className = 'result-panel' + (isError ? ' error' : ' success');
|
|
}
|
|
|
|
window.clearResult = function() {
|
|
const resultEl = document.getElementById('exec-result');
|
|
resultEl.innerHTML = '<span class="empty-state">Execute an action to see results</span>';
|
|
resultEl.className = 'result-panel';
|
|
};
|
|
|
|
window.testQuery = async function() {
|
|
if (!enclave) {
|
|
showQueryResult('Plugin not loaded', true);
|
|
return;
|
|
}
|
|
|
|
const did = document.getElementById('did-input')?.value || '';
|
|
|
|
try {
|
|
const result = await enclave.query(did);
|
|
showQueryResult(JSON.stringify(result, null, 2), false);
|
|
} catch (err) {
|
|
showQueryResult(err?.message || String(err), true);
|
|
}
|
|
};
|
|
|
|
function showQueryResult(content, isError = false) {
|
|
const resultEl = document.getElementById('query-result');
|
|
resultEl.textContent = content;
|
|
resultEl.className = 'result-panel' + (isError ? ' error' : ' success');
|
|
}
|
|
|
|
window.resetEnclave = async function() {
|
|
if (!enclave) return;
|
|
|
|
try {
|
|
await enclave.reset();
|
|
lastDatabase = null;
|
|
log('generate', 'info', 'Enclave state reset');
|
|
clearResult();
|
|
document.getElementById('query-result').innerHTML = '<span class="empty-state">Query a DID to see the document</span>';
|
|
} catch (err) {
|
|
log('generate', 'err', `Reset failed: ${err?.message || String(err)}`);
|
|
}
|
|
};
|
|
|
|
window.runAllTests = async function() {
|
|
log('generate', 'info', '=== Running all tests ===');
|
|
|
|
try {
|
|
await testPing();
|
|
await testGenerateMock();
|
|
await testLoadFromBytes();
|
|
|
|
showSection('explorer');
|
|
await quickExec('accounts', 'list');
|
|
await quickExec('enclaves', 'list');
|
|
|
|
showSection('query');
|
|
await testQuery();
|
|
|
|
log('generate', 'ok', '=== All tests completed ===');
|
|
} catch (err) {
|
|
log('generate', 'err', `Tests failed: ${err?.message || String(err)}`);
|
|
}
|
|
};
|
|
|
|
init();
|