style(example): improve styling and add clear button to card log sections
This commit is contained in:
@@ -19,15 +19,19 @@
|
||||
button:hover { background: #1d4ed8; }
|
||||
button:disabled { background: #374151; cursor: not-allowed; }
|
||||
input { width: 100%; background: #262626; border: 1px solid #404040; color: #fff; padding: 0.5rem; border-radius: 4px; font-family: monospace; font-size: 0.875rem; margin-bottom: 0.5rem; }
|
||||
.log { background: #0a0a0a; border: 1px solid #262626; border-radius: 4px; padding: 0.75rem; font-family: monospace; font-size: 0.75rem; max-height: 300px; overflow-y: auto; white-space: pre-wrap; }
|
||||
.log { background: #0a0a0a; border: 1px solid #262626; border-radius: 4px; padding: 0.5rem; font-family: monospace; font-size: 0.7rem; max-height: 150px; overflow-y: auto; white-space: pre-wrap; margin-top: 0.5rem; display: none; }
|
||||
.log.has-content { display: block; }
|
||||
.log-entry { padding: 0.125rem 0; border-bottom: 1px solid #1a1a1a; }
|
||||
.log-entry:last-child { border-bottom: none; }
|
||||
.log-time { color: #525252; }
|
||||
.log-info { color: #60a5fa; }
|
||||
.log-ok { color: #4ade80; }
|
||||
.log-err { color: #f87171; }
|
||||
.log-data { color: #a78bfa; }
|
||||
.log-data { color: #a78bfa; display: block; margin-left: 1rem; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
|
||||
.clear-btn { background: #374151; padding: 0.25rem 0.5rem; font-size: 0.7rem; margin: 0; }
|
||||
.clear-btn:hover { background: #4b5563; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -41,25 +45,42 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>ping(message)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('ping')">Clear</button>
|
||||
</div>
|
||||
<input type="text" id="ping-msg" value="hello from browser" placeholder="Message to echo">
|
||||
<button onclick="testPing()">Run</button>
|
||||
<div id="log-ping" class="log"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>generate(credential)</h2>
|
||||
<input type="text" id="credential" value="dGVzdC1jcmVkZW50aWFs" placeholder="Base64 credential">
|
||||
<button onclick="testGenerate()">Run</button>
|
||||
<button class="clear-btn" onclick="clearCardLog('generate')">Clear</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button onclick="testGenerate()">Create with WebAuthn</button>
|
||||
<button onclick="testGenerateMock()">Create with Mock</button>
|
||||
</div>
|
||||
<div id="log-generate" class="log"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>load(database)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('load')">Clear</button>
|
||||
</div>
|
||||
<input type="text" id="database" placeholder="Base64 database (auto-filled after generate)">
|
||||
<button onclick="testLoad()">Run</button>
|
||||
<div id="log-load" class="log"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>exec(filter)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('exec')">Clear</button>
|
||||
</div>
|
||||
<input type="text" id="filter" value="resource:accounts action:list" placeholder="resource:X action:Y">
|
||||
<div class="actions">
|
||||
<button onclick="testExec()">Run</button>
|
||||
@@ -67,18 +88,17 @@
|
||||
<button onclick="setFilter('resource:credentials action:list')">Credentials</button>
|
||||
<button onclick="setFilter('resource:sessions action:list')">Sessions</button>
|
||||
</div>
|
||||
<div id="log-exec" class="log"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>query(did)</h2>
|
||||
<button class="clear-btn" onclick="clearCardLog('query')">Clear</button>
|
||||
</div>
|
||||
<input type="text" id="did" placeholder="did:sonr:... (empty = current)">
|
||||
<button onclick="testQuery()">Run</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Log</h2>
|
||||
<button onclick="clearLog()" style="margin-bottom: 0.5rem;">Clear</button>
|
||||
<div id="log" class="log"></div>
|
||||
<div id="log-query" class="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
202
example/main.js
202
example/main.js
@@ -5,21 +5,24 @@ let lastDatabase = null;
|
||||
|
||||
const LogLevel = { INFO: 'info', OK: 'ok', ERR: 'err', DATA: 'data' };
|
||||
|
||||
function log(level, method, message, data = null) {
|
||||
const el = document.getElementById('log');
|
||||
const time = new Date().toISOString().slice(11, 23);
|
||||
const prefix = method ? `[${method}]` : '';
|
||||
function log(card, level, message, data = null) {
|
||||
const el = document.getElementById(`log-${card}`);
|
||||
if (!el) return console.log(`[${card}] ${message}`, data ?? '');
|
||||
|
||||
let entry = `<div class="log-entry"><span class="log-time">${time}</span> <span class="log-${level}">${prefix} ${message}</span>`;
|
||||
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) {
|
||||
entry += `\n<span class="log-data">${JSON.stringify(data, null, 2)}</span>`;
|
||||
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}] ${prefix} ${message}`, data ?? '');
|
||||
console.log(`[${time}] [${card}] ${message}`, data ?? '');
|
||||
}
|
||||
|
||||
function setStatus(ok, message) {
|
||||
@@ -28,119 +31,226 @@ function setStatus(ok, message) {
|
||||
el.className = `status ${ok ? 'ok' : 'err'}`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
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 init() {
|
||||
try {
|
||||
log(LogLevel.INFO, null, 'Loading enclave.wasm...');
|
||||
log('generate', LogLevel.INFO, 'Loading enclave.wasm...');
|
||||
enclave = await createEnclave('./enclave.wasm', { debug: true });
|
||||
setStatus(true, 'Ready');
|
||||
log(LogLevel.OK, null, 'Plugin loaded');
|
||||
log('generate', LogLevel.OK, 'Plugin loaded');
|
||||
} catch (err) {
|
||||
setStatus(false, 'Failed');
|
||||
log(LogLevel.ERR, null, `Load failed: ${err?.message || String(err)}`);
|
||||
log('generate', LogLevel.ERR, `Load failed: ${err?.message || String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
window.testPing = async function() {
|
||||
if (!enclave) return log(LogLevel.ERR, 'ping', 'Plugin not loaded');
|
||||
if (!enclave) return log('ping', LogLevel.ERR, 'Plugin not loaded');
|
||||
|
||||
const message = document.getElementById('ping-msg').value || 'hello';
|
||||
log(LogLevel.INFO, 'ping', `message="${message}"`);
|
||||
log('ping', LogLevel.INFO, `Sending: "${message}"`);
|
||||
|
||||
try {
|
||||
const result = await enclave.ping(message);
|
||||
if (result.success) {
|
||||
log(LogLevel.OK, 'ping', `echo="${result.echo}"`, result);
|
||||
log('ping', LogLevel.OK, `Response: "${result.echo}"`, result);
|
||||
} else {
|
||||
log(LogLevel.ERR, 'ping', result.message, result);
|
||||
log('ping', LogLevel.ERR, result.message, result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log(LogLevel.ERR, 'ping', err?.message || String(err));
|
||||
log('ping', LogLevel.ERR, err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testGenerate = async function() {
|
||||
if (!enclave) return log(LogLevel.ERR, 'generate', 'Plugin not loaded');
|
||||
if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded');
|
||||
|
||||
const credential = document.getElementById('credential').value;
|
||||
log(LogLevel.INFO, 'generate', `credential=${credential.slice(0, 16)}...`);
|
||||
if (!window.PublicKeyCredential) {
|
||||
log('generate', LogLevel.ERR, 'WebAuthn not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await enclave.generate(credential);
|
||||
log(LogLevel.OK, 'generate', `DID created: ${result.did}`, result);
|
||||
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);
|
||||
log('generate', LogLevel.OK, `DID created: ${result.did}`, { did: result.did, dbSize: result.database?.length });
|
||||
|
||||
if (result.database) {
|
||||
lastDatabase = result.database;
|
||||
document.getElementById('database').value = btoa(String.fromCharCode(...result.database));
|
||||
log(LogLevel.INFO, 'generate', 'Database saved for load() test');
|
||||
log('generate', LogLevel.INFO, 'Database saved for load() test');
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log(LogLevel.ERR, 'generate', err?.message || String(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);
|
||||
log('generate', LogLevel.OK, `DID created: ${result.did}`, { did: result.did, dbSize: result.database?.length });
|
||||
|
||||
if (result.database) {
|
||||
lastDatabase = result.database;
|
||||
document.getElementById('database').value = btoa(String.fromCharCode(...result.database));
|
||||
log('generate', LogLevel.INFO, 'Database saved for load() test');
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log('generate', LogLevel.ERR, err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testLoad = async function() {
|
||||
if (!enclave) return log(LogLevel.ERR, 'load', 'Plugin not loaded');
|
||||
if (!enclave) return log('load', LogLevel.ERR, 'Plugin not loaded');
|
||||
|
||||
const b64 = document.getElementById('database').value;
|
||||
if (!b64) return log(LogLevel.ERR, 'load', 'Database required');
|
||||
if (!b64) return log('load', LogLevel.ERR, 'No database - run generate first');
|
||||
|
||||
log(LogLevel.INFO, 'load', `database.length=${b64.length}`);
|
||||
log('load', LogLevel.INFO, `Loading database (${b64.length} chars)...`);
|
||||
|
||||
try {
|
||||
const database = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
||||
const result = await enclave.load(database);
|
||||
|
||||
if (result.success) {
|
||||
log(LogLevel.OK, 'load', `Loaded DID: ${result.did}`, result);
|
||||
log('load', LogLevel.OK, `Loaded DID: ${result.did}`, result);
|
||||
} else {
|
||||
log(LogLevel.ERR, 'load', result.error, result);
|
||||
log('load', LogLevel.ERR, result.error, result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log(LogLevel.ERR, 'load', err?.message || String(err));
|
||||
log('load', LogLevel.ERR, err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testExec = async function() {
|
||||
if (!enclave) return log(LogLevel.ERR, 'exec', 'Plugin not loaded');
|
||||
if (!enclave) return log('exec', LogLevel.ERR, 'Plugin not loaded');
|
||||
|
||||
const filter = document.getElementById('filter').value;
|
||||
if (!filter) return log(LogLevel.ERR, 'exec', 'Filter required');
|
||||
if (!filter) return log('exec', LogLevel.ERR, 'Filter required');
|
||||
|
||||
log(LogLevel.INFO, 'exec', `filter="${filter}"`);
|
||||
log('exec', LogLevel.INFO, `Executing: ${filter}`);
|
||||
|
||||
try {
|
||||
const result = await enclave.exec(filter);
|
||||
|
||||
if (result.success) {
|
||||
log(LogLevel.OK, 'exec', 'Success', result);
|
||||
log('exec', LogLevel.OK, 'Success', result);
|
||||
} else {
|
||||
log(LogLevel.ERR, 'exec', result.error, result);
|
||||
log('exec', LogLevel.ERR, result.error, result);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
log(LogLevel.ERR, 'exec', err?.message || String(err));
|
||||
log('exec', LogLevel.ERR, err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
window.testQuery = async function() {
|
||||
if (!enclave) return log(LogLevel.ERR, 'query', 'Plugin not loaded');
|
||||
if (!enclave) return log('query', LogLevel.ERR, 'Plugin not loaded');
|
||||
|
||||
const did = document.getElementById('did').value;
|
||||
log(LogLevel.INFO, 'query', did ? `did="${did}"` : 'did=(current)');
|
||||
log('query', LogLevel.INFO, did ? `Querying: ${did}` : 'Querying current DID...');
|
||||
|
||||
try {
|
||||
const result = await enclave.query(did);
|
||||
log(LogLevel.OK, 'query', `Resolved: ${result.did}`, result);
|
||||
log('query', LogLevel.OK, `Resolved: ${result.did}`, result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
log(LogLevel.ERR, 'query', err?.message || String(err));
|
||||
log('query', LogLevel.ERR, err?.message || String(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -149,22 +259,26 @@ window.setFilter = function(filter) {
|
||||
document.getElementById('filter').value = filter;
|
||||
};
|
||||
|
||||
window.clearLog = function() {
|
||||
document.getElementById('log').innerHTML = '';
|
||||
window.clearCardLog = function(card) {
|
||||
const el = document.getElementById(`log-${card}`);
|
||||
if (el) {
|
||||
el.innerHTML = '';
|
||||
el.classList.remove('has-content');
|
||||
}
|
||||
};
|
||||
|
||||
window.runAllTests = async function() {
|
||||
log(LogLevel.INFO, null, '=== Running all tests ===');
|
||||
log('ping', LogLevel.INFO, '=== Running all tests ===');
|
||||
|
||||
try {
|
||||
await testPing();
|
||||
await testGenerate();
|
||||
await testGenerateMock();
|
||||
await testLoad();
|
||||
await testExec();
|
||||
await testQuery();
|
||||
log(LogLevel.OK, null, '=== All tests passed ===');
|
||||
log('query', LogLevel.OK, '=== All tests passed ===');
|
||||
} catch (err) {
|
||||
log(LogLevel.ERR, null, `=== Tests failed: ${err?.message || String(err)} ===`);
|
||||
log('query', LogLevel.ERR, `Tests failed: ${err?.message || String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user