diff --git a/example/index.html b/example/index.html index 4dff646..df544c0 100644 --- a/example/index.html +++ b/example/index.html @@ -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; } @@ -41,25 +45,42 @@
-

ping(message)

+
+

ping(message)

+ +
+
-

generate(credential)

- - +
+

generate(credential)

+ +
+
+ + +
+
-

load(database)

+
+

load(database)

+ +
+
-

exec(filter)

+
+

exec(filter)

+ +
@@ -67,18 +88,17 @@
+
-

query(did)

+
+

query(did)

+ +
-
- -
-

Log

- -
+
diff --git a/example/main.js b/example/main.js index bfd7928..e92f33d 100644 --- a/example/main.js +++ b/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 = `
${time} ${prefix} ${message}`; + const time = new Date().toISOString().slice(11, 23); + + let entry = `
${time} ${message}`; if (data !== null) { - entry += `\n${JSON.stringify(data, null, 2)}`; + const json = typeof data === 'string' ? data : JSON.stringify(data, null, 2); + entry += `${json}`; } entry += '
'; 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)}`); } };