| … | … | @@ -0,0 +1,15 @@ |
| 1 | # Dependencies | |
| 2 | node_modules/ | |
| 3 | ||
| 4 | # Storage directories created by example CLIs | |
| 5 | .anproto-*/ | |
| 6 | ||
| 7 | # Editor and OS cruft | |
| 8 | .DS_Store | |
| 9 | *.log | |
| 10 | *.swp | |
| 11 | .vscode/ | |
| 12 | .idea/ | |
| 13 | ||
| 14 | # Test artifacts | |
| 15 | coverage/ |
| … | … | @@ -0,0 +1,110 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | /** | |
| 3 | * Mini CLI example for anproto | |
| 4 | * | |
| 5 | * Demonstrates the canonical 4 functions: gen → hash → sign → open | |
| 6 | * | |
| 7 | * Usage: | |
| 8 | * node examples/cli.js gen Generate a new keypair | |
| 9 | * node examples/cli.js hash <data> Hash some data | |
| 10 | * node examples/cli.js sign <hash> <keypair> Sign a hash | |
| 11 | * node examples/cli.js open <signature> Open a signature | |
| 12 | * node examples/cli.js demo Run a full round-trip | |
| 13 | * node examples/cli.js challenges Demo the Challenges helper | |
| 14 | */ | |
| 15 | ||
| 16 | import { an, Challenges } from '../index.js' | |
| 17 | ||
| 18 | const args = process.argv.slice(2) | |
| 19 | const cmd = args[0] | |
| 20 | ||
| 21 | async function main () { | |
| 22 | if (cmd === 'gen') { | |
| 23 | const k = await an.gen() | |
| 24 | console.log('keypair: %s', k) | |
| 25 | console.log('pubkey: %s', an.publicKey(k)) | |
| 26 | } else if (cmd === 'hash') { | |
| 27 | const data = args[1] | |
| 28 | if (!data) { console.error('Usage: cli.js hash <data>'); process.exit(1) } | |
| 29 | const h = await an.hash(data) | |
| 30 | console.log(h) | |
| 31 | } else if (cmd === 'sign') { | |
| 32 | const h = args[1] | |
| 33 | const k = args[2] | |
| 34 | if (!h || !k) { console.error('Usage: cli.js sign <hash> <keypair>'); process.exit(1) } | |
| 35 | const sig = await an.sign(h, k) | |
| 36 | console.log(sig) | |
| 37 | } else if (cmd === 'open') { | |
| 38 | const sig = args[1] | |
| 39 | if (!sig) { console.error('Usage: cli.js open <signature>'); process.exit(1) } | |
| 40 | try { | |
| 41 | const opened = await an.open(sig) | |
| 42 | const parsed = an.parse(opened) | |
| 43 | console.log('opened: %s', opened) | |
| 44 | console.log('timestamp: %d (%s)', parsed.timestamp, new Date(parsed.timestamp).toISOString()) | |
| 45 | console.log('hash: %s', parsed.hash) | |
| 46 | } catch (err) { | |
| 47 | console.error('Invalid signature: %s', err.message) | |
| 48 | process.exit(1) | |
| 49 | } | |
| 50 | } else if (cmd === 'demo') { | |
| 51 | console.log('--- ANProto round-trip demo ---\n') | |
| 52 | ||
| 53 | const message = args[1] || 'Hello, ANProto!' | |
| 54 | console.log('1. Generate keypair') | |
| 55 | const k = await an.gen() | |
| 56 | console.log(' keypair: %s\n', k) | |
| 57 | ||
| 58 | console.log('2. Hash message: %j', message) | |
| 59 | const h = await an.hash(message) | |
| 60 | console.log(' hash: %s\n', h) | |
| 61 | ||
| 62 | console.log('3. Sign hash') | |
| 63 | const sig = await an.sign(h, k) | |
| 64 | console.log(' sig: %s\n', sig) | |
| 65 | ||
| 66 | console.log('4. Open signature') | |
| 67 | const opened = await an.open(sig) | |
| 68 | const parsed = an.parse(opened) | |
| 69 | console.log(' opened: %s', opened) | |
| 70 | console.log(' timestamp: %d (%s)', parsed.timestamp, new Date(parsed.timestamp).toISOString()) | |
| 71 | console.log(' hash: %s\n', parsed.hash) | |
| 72 | ||
| 73 | console.log('5. Verify hash matches') | |
| 74 | console.log(' match: %s', parsed.hash === h ? 'YES ✓' : 'NO ✗') | |
| 75 | } else if (cmd === 'challenges') { | |
| 76 | console.log('--- Challenges helper demo ---\n') | |
| 77 | const c = new Challenges({ ttl: 5000 }) | |
| 78 | ||
| 79 | console.log('Generate 3 challenges:') | |
| 80 | const c1 = c.generate() | |
| 81 | const c2 = c.generate() | |
| 82 | const c3 = c.generate() | |
| 83 | console.log(' c1: %s', c1) | |
| 84 | console.log(' c2: %s', c2) | |
| 85 | console.log(' c3: %s', c3) | |
| 86 | console.log(' active: %d\n', c.size) | |
| 87 | ||
| 88 | console.log('Consume c2:') | |
| 89 | console.log(' consume(c2): %s', c.consume(c2)) | |
| 90 | console.log(' consume(c2): %s (single-use)', c.consume(c2)) | |
| 91 | console.log(' active: %d\n', c.size) | |
| 92 | ||
| 93 | console.log('Verify c1 still active:') | |
| 94 | console.log(' has(c1): %s', c.has(c1)) | |
| 95 | console.log(' has(c2): %s', c.has(c2)) | |
| 96 | console.log(' has(c3): %s', c.has(c3)) | |
| 97 | ||
| 98 | c.destroy() | |
| 99 | } else { | |
| 100 | console.log('anproto example CLI') | |
| 101 | console.log(' gen Generate a keypair') | |
| 102 | console.log(' hash <data> Hash data') | |
| 103 | console.log(' sign <hash> <keypair> Sign a hash') | |
| 104 | console.log(' open <signature> Open a signature') | |
| 105 | console.log(' demo [message] Full round-trip demo') | |
| 106 | console.log(' challenges Replay-protection helper demo') | |
| 107 | } | |
| 108 | } | |
| 109 | ||
| 110 | main().catch((err) => { console.error(err.message); process.exit(1) }) |
| … | … | @@ -0,0 +1,356 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | /** | |
| 3 | * Mini web example for anproto | |
| 4 | * | |
| 5 | * Demonstrates a complete SSR browser login flow: | |
| 6 | * 1. Browser visits / and gets a login page with a fresh challenge | |
| 7 | * 2. Browser generates an Ed25519 keypair (stored in localStorage) | |
| 8 | * 3. Browser hashes the challenge and signs it (canonical anproto) | |
| 9 | * 4. Browser POSTs the signature to /login | |
| 10 | * 5. Server verifies the signature, consumes the challenge, sets a cookie | |
| 11 | * 6. Subsequent requests use the cookie to identify the user | |
| 12 | * | |
| 13 | * Usage: node examples/web.js [--port 9093] | |
| 14 | * | |
| 15 | * Browser side uses noble-ed25519 (vendored from canonical anproto via | |
| 16 | * lib/nacl-fast-es.js) — but for the demo we use a tiny inline shim that | |
| 17 | * uses WebCrypto's Ed25519 (Chrome 137+, Firefox 130+, Safari 17+). | |
| 18 | */ | |
| 19 | ||
| 20 | import http from 'http' | |
| 21 | import { an, Challenges } from '../index.js' | |
| 22 | import b4a from 'b4a' | |
| 23 | ||
| 24 | const args = process.argv.slice(2) | |
| 25 | function getFlag (name) { | |
| 26 | const i = args.indexOf('--' + name) | |
| 27 | return i !== -1 && args[i + 1] ? args[i + 1] : null | |
| 28 | } | |
| 29 | ||
| 30 | const port = parseInt(getFlag('port')) || 9093 | |
| 31 | ||
| 32 | // ── State ───────────────────────────────────────────────────── | |
| 33 | ||
| 34 | const challenges = new Challenges({ ttl: 5 * 60 * 1000 }) | |
| 35 | // pubkey → { firstSeen, lastSeen, loginCount } | |
| 36 | const sessions = new Map() | |
| 37 | // Cookie value → pubkey | |
| 38 | const cookieToPubkey = new Map() | |
| 39 | ||
| 40 | function newCookieValue () { | |
| 41 | return Math.random().toString(36).slice(2) + Date.now().toString(36) | |
| 42 | } | |
| 43 | ||
| 44 | function parseCookies (req) { | |
| 45 | const out = {} | |
| 46 | const header = req.headers.cookie | |
| 47 | if (!header) return out | |
| 48 | for (const part of header.split(';')) { | |
| 49 | const [k, v] = part.trim().split('=') | |
| 50 | out[k] = v | |
| 51 | } | |
| 52 | return out | |
| 53 | } | |
| 54 | ||
| 55 | function escapeHtml (s) { | |
| 56 | return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | |
| 57 | } | |
| 58 | ||
| 59 | // ── HTTP server ─────────────────────────────────────────────── | |
| 60 | ||
| 61 | const httpServer = http.createServer(async (req, res) => { | |
| 62 | const url = req.url.split('?')[0] | |
| 63 | const cookies = parseCookies(req) | |
| 64 | const sessionPubkey = cookies.session ? cookieToPubkey.get(cookies.session) : null | |
| 65 | ||
| 66 | // ── GET / — landing page ───────────────────────────────────── | |
| 67 | ||
| 68 | if (req.method === 'GET' && url === '/') { | |
| 69 | const challenge = challenges.generate() | |
| 70 | res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) | |
| 71 | res.end(renderHomePage(challenge, sessionPubkey)) | |
| 72 | return | |
| 73 | } | |
| 74 | ||
| 75 | // ── POST /login — verify the signature ────────────────────── | |
| 76 | ||
| 77 | if (req.method === 'POST' && url === '/login') { | |
| 78 | const body = await collectBody(req) | |
| 79 | const params = new URLSearchParams(body) | |
| 80 | const challenge = params.get('challenge') | |
| 81 | const signature = params.get('signature') | |
| 82 | ||
| 83 | if (!challenge || !signature) { | |
| 84 | res.writeHead(400, { 'Content-Type': 'text/plain' }) | |
| 85 | res.end('Missing challenge or signature') | |
| 86 | return | |
| 87 | } | |
| 88 | ||
| 89 | // Verify the challenge is one we issued and not yet consumed | |
| 90 | if (!challenges.consume(challenge)) { | |
| 91 | res.writeHead(400, { 'Content-Type': 'text/plain' }) | |
| 92 | res.end('Unknown or expired challenge') | |
| 93 | return | |
| 94 | } | |
| 95 | ||
| 96 | // Verify the signature is valid ANProto | |
| 97 | let opened | |
| 98 | try { | |
| 99 | opened = await an.open(signature) | |
| 100 | } catch (err) { | |
| 101 | res.writeHead(400, { 'Content-Type': 'text/plain' }) | |
| 102 | res.end('Invalid signature: ' + err.message) | |
| 103 | return | |
| 104 | } | |
| 105 | ||
| 106 | // Verify the signed hash matches the challenge we issued | |
| 107 | const expectedHash = await an.hash(challenge) | |
| 108 | const parsed = an.parse(opened) | |
| 109 | if (parsed.hash !== expectedHash) { | |
| 110 | res.writeHead(400, { 'Content-Type': 'text/plain' }) | |
| 111 | res.end('Signature does not match challenge hash') | |
| 112 | return | |
| 113 | } | |
| 114 | ||
| 115 | // Verify the timestamp is recent (defence in depth — challenge consumption | |
| 116 | // already prevents replay, but the timestamp catches stale signatures) | |
| 117 | if (Math.abs(Date.now() - parsed.timestamp) > 5 * 60 * 1000) { | |
| 118 | res.writeHead(400, { 'Content-Type': 'text/plain' }) | |
| 119 | res.end('Signature timestamp out of window') | |
| 120 | return | |
| 121 | } | |
| 122 | ||
| 123 | // Extract the pubkey from the signature | |
| 124 | const pubkey = signature.substring(0, 44) | |
| 125 | ||
| 126 | // Update session info | |
| 127 | const existing = sessions.get(pubkey) | |
| 128 | sessions.set(pubkey, { | |
| 129 | firstSeen: existing ? existing.firstSeen : Date.now(), | |
| 130 | lastSeen: Date.now(), | |
| 131 | loginCount: (existing?.loginCount || 0) + 1 | |
| 132 | }) | |
| 133 | ||
| 134 | // Issue a session cookie | |
| 135 | const cookieValue = newCookieValue() | |
| 136 | cookieToPubkey.set(cookieValue, pubkey) | |
| 137 | ||
| 138 | res.writeHead(303, { | |
| 139 | 'Set-Cookie': `session=${cookieValue}; HttpOnly; Path=/; Max-Age=${60 * 60 * 24}`, | |
| 140 | Location: '/' | |
| 141 | }) | |
| 142 | res.end() | |
| 143 | return | |
| 144 | } | |
| 145 | ||
| 146 | // ── POST /logout ───────────────────────────────────────────── | |
| 147 | ||
| 148 | if (req.method === 'POST' && url === '/logout') { | |
| 149 | if (cookies.session) cookieToPubkey.delete(cookies.session) | |
| 150 | res.writeHead(303, { | |
| 151 | 'Set-Cookie': 'session=; HttpOnly; Path=/; Max-Age=0', | |
| 152 | Location: '/' | |
| 153 | }) | |
| 154 | res.end() | |
| 155 | return | |
| 156 | } | |
| 157 | ||
| 158 | // ── 404 ────────────────────────────────────────────────────── | |
| 159 | ||
| 160 | res.writeHead(404, { 'Content-Type': 'text/plain' }) | |
| 161 | res.end('Not found') | |
| 162 | }) | |
| 163 | ||
| 164 | function collectBody (req) { | |
| 165 | return new Promise((resolve, reject) => { | |
| 166 | const parts = [] | |
| 167 | req.on('data', chunk => parts.push(chunk)) | |
| 168 | req.on('end', () => resolve(b4a.concat(parts).toString('utf8'))) | |
| 169 | req.on('error', reject) | |
| 170 | }) | |
| 171 | } | |
| 172 | ||
| 173 | // ── HTML pages (SSR — no JSON API) ──────────────────────────── | |
| 174 | ||
| 175 | function renderHomePage (challenge, sessionPubkey) { | |
| 176 | const session = sessionPubkey ? sessions.get(sessionPubkey) : null | |
| 177 | ||
| 178 | return `<!DOCTYPE html> | |
| 179 | <html><head><title>anproto demo</title> | |
| 180 | <style> | |
| 181 | body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 720px; margin: 0 auto; } | |
| 182 | h1 { color: #58a6ff; } | |
| 183 | .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; } | |
| 184 | .card h2 { font-size: 0.75rem; text-transform: uppercase; color: #8b949e; margin: 0 0 0.5rem; } | |
| 185 | .row { display: flex; justify-content: space-between; padding: 0.3rem 0; border-bottom: 1px solid #21262d; } | |
| 186 | .row:last-child { border-bottom: none; } | |
| 187 | .label { color: #8b949e; } | |
| 188 | .value { font-family: monospace; color: #79c0ff; word-break: break-all; max-width: 70%; text-align: right; } | |
| 189 | .btn { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem; font-family: inherit; } | |
| 190 | .btn:hover { background: #30363d; } | |
| 191 | .btn-primary { background: #1f6feb; border-color: #1f6feb; color: white; } | |
| 192 | .btn-primary:hover { background: #1158c7; } | |
| 193 | #log { font-size: 0.8rem; color: #8b949e; min-height: 4em; } | |
| 194 | #log div { padding: 2px 0; } | |
| 195 | .ok { color: #3fb950; } | |
| 196 | .err { color: #f85149; } | |
| 197 | </style></head><body> | |
| 198 | <h1>anproto SSR login demo</h1> | |
| 199 | ||
| 200 | ${sessionPubkey ? renderLoggedIn(sessionPubkey, session) : renderLoginForm(challenge)} | |
| 201 | ||
| 202 | <div class="card"> | |
| 203 | <h2>How this works</h2> | |
| 204 | <ol style="line-height: 1.6; font-size: 0.85rem;"> | |
| 205 | <li>Server issues a random <strong>challenge</strong> in the page</li> | |
| 206 | <li>Browser generates an Ed25519 keypair (stored in localStorage)</li> | |
| 207 | <li>Browser hashes the challenge and signs it via canonical ANProto</li> | |
| 208 | <li>Browser POSTs the signature to <code>/login</code></li> | |
| 209 | <li>Server verifies the signature, consumes the challenge, sets a cookie</li> | |
| 210 | </ol> | |
| 211 | </div> | |
| 212 | ||
| 213 | </body></html>` | |
| 214 | } | |
| 215 | ||
| 216 | function renderLoginForm (challenge) { | |
| 217 | return `<div class="card"> | |
| 218 | <h2>Not logged in</h2> | |
| 219 | <div class="row"><span class="label">Challenge</span><span class="value">${escapeHtml(challenge)}</span></div> | |
| 220 | <div style="margin-top: 1rem"> | |
| 221 | <button class="btn btn-primary" id="login-btn">Sign in with Ed25519</button> | |
| 222 | <button class="btn" id="reset-btn">Generate new keypair</button> | |
| 223 | <span id="log"></span> | |
| 224 | </div> | |
| 225 | </div> | |
| 226 | ||
| 227 | <script type="module"> | |
| 228 | const log = (msg, cls = '') => { | |
| 229 | const d = document.createElement('div') | |
| 230 | d.textContent = msg | |
| 231 | if (cls) d.className = cls | |
| 232 | document.getElementById('log').appendChild(d) | |
| 233 | } | |
| 234 | ||
| 235 | // ── Browser ANProto using WebCrypto Ed25519 ── | |
| 236 | // | |
| 237 | // We re-implement the canonical ANProto wire format on the browser side. | |
| 238 | // Modern browsers (Chrome 137+, Firefox 130+, Safari 17+) have Ed25519 in | |
| 239 | // WebCrypto. For older browsers, swap in noble-ed25519 (~10 KB JS bundle). | |
| 240 | ||
| 241 | const enc = new TextEncoder() | |
| 242 | const dec = new TextDecoder() | |
| 243 | ||
| 244 | const b64encode = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))) | |
| 245 | const b64decode = (s) => Uint8Array.from(atob(s), c => c.charCodeAt(0)) | |
| 246 | ||
| 247 | async function gen() { | |
| 248 | const kp = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']) | |
| 249 | // Export raw pubkey (32 bytes) | |
| 250 | const pubRaw = new Uint8Array(await crypto.subtle.exportKey('raw', kp.publicKey)) | |
| 251 | // Export pkcs8 secret key, extract the seed | |
| 252 | const secPkcs8 = new Uint8Array(await crypto.subtle.exportKey('pkcs8', kp.privateKey)) | |
| 253 | // pkcs8 format: ASN.1 wrapping. The 32-byte seed is at the end (last 32 bytes of the OCTET STRING) | |
| 254 | // Quick extraction: seed is the last 32 bytes of the pkcs8 buffer | |
| 255 | const seed = secPkcs8.slice(-32) | |
| 256 | // ANProto secret key = seed (32 bytes) || pubkey (32 bytes) — that's the libsodium 64-byte secret key format | |
| 257 | const sec = new Uint8Array(64) | |
| 258 | sec.set(seed, 0) | |
| 259 | sec.set(pubRaw, 32) | |
| 260 | return b64encode(pubRaw) + b64encode(sec) | |
| 261 | } | |
| 262 | ||
| 263 | async function hash(data) { | |
| 264 | const buf = enc.encode(data) | |
| 265 | const h = await crypto.subtle.digest('SHA-256', buf) | |
| 266 | return b64encode(h) | |
| 267 | } | |
| 268 | ||
| 269 | async function sign(h, keypair) { | |
| 270 | const ts = Date.now() | |
| 271 | const message = enc.encode(String(ts) + h) | |
| 272 | // Reconstruct the WebCrypto private key from the seed (first 32 of the 64-byte sec key) | |
| 273 | const secBytes = b64decode(keypair.substring(44)) | |
| 274 | const seed = secBytes.slice(0, 32) | |
| 275 | // Build a minimal pkcs8 wrapper for an Ed25519 seed | |
| 276 | const pkcs8 = new Uint8Array([ | |
| 277 | 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20, | |
| 278 | ...seed | |
| 279 | ]) | |
| 280 | const privateKey = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, false, ['sign']) | |
| 281 | const sigBytes = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, message)) | |
| 282 | // Combined-mode envelope: sig (64) || message | |
| 283 | const combined = new Uint8Array(sigBytes.length + message.length) | |
| 284 | combined.set(sigBytes, 0) | |
| 285 | combined.set(message, sigBytes.length) | |
| 286 | return keypair.substring(0, 44) + b64encode(combined) | |
| 287 | } | |
| 288 | ||
| 289 | // ── Login flow ── | |
| 290 | ||
| 291 | document.getElementById('reset-btn').onclick = () => { | |
| 292 | localStorage.removeItem('anproto-keypair') | |
| 293 | log('Keypair cleared from localStorage') | |
| 294 | } | |
| 295 | ||
| 296 | document.getElementById('login-btn').onclick = async () => { | |
| 297 | try { | |
| 298 | let kp = localStorage.getItem('anproto-keypair') | |
| 299 | if (!kp) { | |
| 300 | log('No keypair in localStorage, generating new one...') | |
| 301 | kp = await gen() | |
| 302 | localStorage.setItem('anproto-keypair', kp) | |
| 303 | log('Generated keypair (saved to localStorage)', 'ok') | |
| 304 | log(' pubkey: ' + kp.substring(0, 44)) | |
| 305 | } else { | |
| 306 | log('Loaded existing keypair from localStorage', 'ok') | |
| 307 | log(' pubkey: ' + kp.substring(0, 44)) | |
| 308 | } | |
| 309 | ||
| 310 | const challenge = ${JSON.stringify(challenge)} | |
| 311 | log('Hashing challenge...') | |
| 312 | const h = await hash(challenge) | |
| 313 | log(' hash: ' + h) | |
| 314 | ||
| 315 | log('Signing hash...') | |
| 316 | const sig = await sign(h, kp) | |
| 317 | log(' sig: ' + sig.substring(0, 60) + '...') | |
| 318 | ||
| 319 | log('POSTing /login...') | |
| 320 | const body = new URLSearchParams({ challenge, signature: sig }) | |
| 321 | const res = await fetch('/login', { | |
| 322 | method: 'POST', | |
| 323 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
| 324 | body | |
| 325 | }) | |
| 326 | if (res.ok || res.redirected) { | |
| 327 | log('Login successful — reloading', 'ok') | |
| 328 | window.location.href = '/' | |
| 329 | } else { | |
| 330 | const text = await res.text() | |
| 331 | log('Login failed: ' + text, 'err') | |
| 332 | } | |
| 333 | } catch (err) { | |
| 334 | log('Error: ' + err.message, 'err') | |
| 335 | } | |
| 336 | } | |
| 337 | </script>` | |
| 338 | } | |
| 339 | ||
| 340 | function renderLoggedIn (pubkey, session) { | |
| 341 | return `<div class="card"> | |
| 342 | <h2>Logged in</h2> | |
| 343 | <div class="row"><span class="label">Pubkey</span><span class="value">${escapeHtml(pubkey)}</span></div> | |
| 344 | <div class="row"><span class="label">First seen</span><span class="value">${session ? new Date(session.firstSeen).toLocaleString() : '?'}</span></div> | |
| 345 | <div class="row"><span class="label">Last seen</span><span class="value">${session ? new Date(session.lastSeen).toLocaleString() : '?'}</span></div> | |
| 346 | <div class="row"><span class="label">Login count</span><span class="value">${session ? session.loginCount : 1}</span></div> | |
| 347 | <form action="/logout" method="POST" style="margin-top: 1rem"> | |
| 348 | <button class="btn" type="submit">Log out</button> | |
| 349 | </form> | |
| 350 | </div>` | |
| 351 | } | |
| 352 | ||
| 353 | httpServer.listen(port, () => { | |
| 354 | console.log('anproto SSR demo running:') | |
| 355 | console.log(' http://localhost:%d', port) | |
| 356 | }) |
| … | … | @@ -0,0 +1,12 @@ |
| 1 | /** | |
| 2 | * anproto — wire-compatible implementation of evbogue's ANProto | |
| 3 | * | |
| 4 | * https://anproto.com — the Authenticated Non-networked protocol | |
| 5 | * | |
| 6 | * Exports: | |
| 7 | * an — the canonical 4 functions: gen, hash, sign, open (+ helpers) | |
| 8 | * Challenges — replay-protection helper for HTTP login flows (not part of spec) | |
| 9 | */ | |
| 10 | ||
| 11 | export { an } from './lib/an.js' | |
| 12 | export { Challenges } from './lib/challenges.js' |
| … | … | @@ -0,0 +1,125 @@ |
| 1 | /** | |
| 2 | * an.js — wire-compatible implementation of evbogue's ANProto | |
| 3 | * | |
| 4 | * Spec: https://anproto.com (canonical at github.com/evbogue/ANProto) | |
| 5 | * | |
| 6 | * Same wire format as the canonical TweetNaCl-based implementation, but | |
| 7 | * uses sodium-native (via hypercore-crypto) so we don't carry a second | |
| 8 | * Ed25519 implementation. Both libraries implement the same Ed25519 | |
| 9 | * standard and produce byte-identical signatures for the same inputs. | |
| 10 | * | |
| 11 | * The four canonical functions: | |
| 12 | * | |
| 13 | * gen() → 132-char keypair string (44 pubkey b64 + 88 secret b64) | |
| 14 | * hash(data) → 44-char base64 SHA-256 | |
| 15 | * sign(hash, kp) → pubkey(44) + base64(combined-mode-sign(timestamp + hash)) | |
| 16 | * open(message) → "{timestamp}{hash}" string (throws if invalid) | |
| 17 | */ | |
| 18 | ||
| 19 | import crypto from 'hypercore-crypto' | |
| 20 | import sodium from 'sodium-native' | |
| 21 | import b4a from 'b4a' | |
| 22 | ||
| 23 | const SIG_BYTES = 64 // Ed25519 detached signature size (crypto_sign_BYTES) | |
| 24 | ||
| 25 | export const an = {} | |
| 26 | ||
| 27 | /** | |
| 28 | * Generate a new Ed25519 keypair. | |
| 29 | * @returns {Promise<string>} 132-char string: base64(pubkey 32B) + base64(secret 64B) | |
| 30 | */ | |
| 31 | an.gen = async () => { | |
| 32 | const { publicKey, secretKey } = crypto.keyPair() | |
| 33 | return b4a.toString(publicKey, 'base64') + b4a.toString(secretKey, 'base64') | |
| 34 | } | |
| 35 | ||
| 36 | /** | |
| 37 | * SHA-256 hash data. | |
| 38 | * @param {string|Buffer} data | |
| 39 | * @returns {Promise<string>} 44-char base64 SHA-256 hash | |
| 40 | */ | |
| 41 | an.hash = async (data) => { | |
| 42 | const buf = typeof data === 'string' ? b4a.from(data, 'utf8') : data | |
| 43 | const out = b4a.alloc(32) | |
| 44 | sodium.crypto_hash_sha256(out, buf) | |
| 45 | return b4a.toString(out, 'base64') | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Sign a hash with a keypair, producing a wire-format ANProto signature. | |
| 50 | * The signed message is "{Date.now()}{hash}" — timestamp prefixed. | |
| 51 | * | |
| 52 | * Wire format: pubkey(44 b64) + base64(combined_mode_sign(message)) | |
| 53 | * where combined_mode_sign(m) = signature(64B) || m | |
| 54 | * | |
| 55 | * @param {string} hash - 44-char base64 hash from an.hash() | |
| 56 | * @param {string} keypair - 132-char keypair string from an.gen() | |
| 57 | * @returns {Promise<string>} wire-format signature | |
| 58 | */ | |
| 59 | an.sign = async (hash, keypair) => { | |
| 60 | const ts = Date.now() | |
| 61 | const message = b4a.from(String(ts) + hash, 'utf8') | |
| 62 | const secretKey = b4a.from(keypair.substring(44), 'base64') | |
| 63 | const sig = crypto.sign(message, secretKey) // detached, 64 bytes | |
| 64 | // Combined-mode envelope: signature || message | |
| 65 | const combined = b4a.concat([sig, message]) | |
| 66 | return keypair.substring(0, 44) + b4a.toString(combined, 'base64') | |
| 67 | } | |
| 68 | ||
| 69 | /** | |
| 70 | * Verify and open an ANProto signature, returning the embedded message. | |
| 71 | * The returned string is "{timestamp}{hash}" — caller can split at offset 13. | |
| 72 | * | |
| 73 | * @param {string} message - wire-format signature from an.sign() | |
| 74 | * @returns {Promise<string>} the original signed message ("{timestamp}{hash}") | |
| 75 | * @throws {Error} if the signature is invalid | |
| 76 | */ | |
| 77 | an.open = async (message) => { | |
| 78 | if (typeof message !== 'string' || message.length < 44) { | |
| 79 | throw new Error('Invalid ANProto message (too short)') | |
| 80 | } | |
| 81 | const publicKey = b4a.from(message.substring(0, 44), 'base64') | |
| 82 | const combined = b4a.from(message.substring(44), 'base64') | |
| 83 | if (publicKey.length !== 32) throw new Error('Invalid ANProto pubkey length') | |
| 84 | if (combined.length < SIG_BYTES) throw new Error('Invalid ANProto combined-mode envelope') | |
| 85 | ||
| 86 | const sig = combined.subarray(0, SIG_BYTES) | |
| 87 | const body = combined.subarray(SIG_BYTES) | |
| 88 | ||
| 89 | if (!crypto.verify(body, sig, publicKey)) { | |
| 90 | throw new Error('Invalid ANProto signature') | |
| 91 | } | |
| 92 | return b4a.toString(body, 'utf8') | |
| 93 | } | |
| 94 | ||
| 95 | // ── Convenience helpers (not part of canonical spec, but commonly needed) ── | |
| 96 | ||
| 97 | /** | |
| 98 | * Extract the public key from a keypair string. | |
| 99 | * @param {string} keypair - 132-char keypair string | |
| 100 | * @returns {string} 44-char base64 public key | |
| 101 | */ | |
| 102 | an.publicKey = (keypair) => { | |
| 103 | if (typeof keypair !== 'string' || keypair.length !== 132) { | |
| 104 | throw new Error('Invalid keypair (expected 132 chars)') | |
| 105 | } | |
| 106 | return keypair.substring(0, 44) | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Parse an opened message into its timestamp and hash components. | |
| 111 | * Canonical format: 13-digit timestamp followed by 44-char base64 hash. | |
| 112 | * @param {string} opened - the string returned by an.open() | |
| 113 | * @returns {{ timestamp: number, hash: string }} | |
| 114 | */ | |
| 115 | an.parse = (opened) => { | |
| 116 | if (typeof opened !== 'string' || opened.length < 13) { | |
| 117 | throw new Error('Invalid opened message') | |
| 118 | } | |
| 119 | const tsMatch = opened.match(/^(\d{13})(.+)$/) | |
| 120 | if (!tsMatch) throw new Error('Invalid opened message format') | |
| 121 | return { | |
| 122 | timestamp: parseInt(tsMatch[1], 10), | |
| 123 | hash: tsMatch[2] | |
| 124 | } | |
| 125 | } |
| … | … | @@ -0,0 +1,93 @@ |
| 1 | /** | |
| 2 | * Challenges — in-memory replay protection helper for ANProto login flows | |
| 3 | * | |
| 4 | * Not part of canonical ANProto. A small helper for HTTP login flows where | |
| 5 | * the server issues a challenge string, the client signs hash(challenge), | |
| 6 | * and the server consumes the challenge to prevent replay. | |
| 7 | * | |
| 8 | * Usage: | |
| 9 | * const challenges = new Challenges({ ttl: 5 * 60 * 1000 }) | |
| 10 | * const challenge = challenges.generate() // server issues | |
| 11 | * // ... browser signs hash(challenge), POSTs sig ... | |
| 12 | * if (!challenges.consume(challenge)) reject() // server consumes (single-use) | |
| 13 | */ | |
| 14 | ||
| 15 | import crypto from 'hypercore-crypto' | |
| 16 | import b4a from 'b4a' | |
| 17 | ||
| 18 | const DEFAULT_TTL = 5 * 60 * 1000 // 5 minutes | |
| 19 | const DEFAULT_CLEANUP_INTERVAL = 60 * 1000 // 1 minute | |
| 20 | ||
| 21 | export class Challenges { | |
| 22 | constructor (opts = {}) { | |
| 23 | this.ttl = opts.ttl || DEFAULT_TTL | |
| 24 | this._active = new Map() // challenge string → expiresAt | |
| 25 | this._cleanupTimer = setInterval(() => this._cleanup(), opts.cleanupInterval || DEFAULT_CLEANUP_INTERVAL) | |
| 26 | if (this._cleanupTimer.unref) this._cleanupTimer.unref() | |
| 27 | } | |
| 28 | ||
| 29 | /** | |
| 30 | * Issue a new challenge string. The caller sends this to the client; the | |
| 31 | * client signs hash(challenge) and returns the signature. | |
| 32 | * @returns {string} 44-char base64 random challenge | |
| 33 | */ | |
| 34 | generate () { | |
| 35 | const challenge = b4a.toString(crypto.randomBytes(32), 'base64') | |
| 36 | this._active.set(challenge, Date.now() + this.ttl) | |
| 37 | return challenge | |
| 38 | } | |
| 39 | ||
| 40 | /** | |
| 41 | * Consume a challenge. Returns true if the challenge was active and not | |
| 42 | * yet consumed; false otherwise. Single-use — calling consume() removes | |
| 43 | * the challenge from the active set. | |
| 44 | * @param {string} challenge | |
| 45 | * @returns {boolean} | |
| 46 | */ | |
| 47 | consume (challenge) { | |
| 48 | const expiresAt = this._active.get(challenge) | |
| 49 | if (expiresAt === undefined) return false | |
| 50 | if (expiresAt < Date.now()) { | |
| 51 | this._active.delete(challenge) | |
| 52 | return false | |
| 53 | } | |
| 54 | this._active.delete(challenge) | |
| 55 | return true | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * Check if a challenge is active without consuming it. | |
| 60 | * @param {string} challenge | |
| 61 | * @returns {boolean} | |
| 62 | */ | |
| 63 | has (challenge) { | |
| 64 | const expiresAt = this._active.get(challenge) | |
| 65 | if (expiresAt === undefined) return false | |
| 66 | return expiresAt >= Date.now() | |
| 67 | } | |
| 68 | ||
| 69 | /** | |
| 70 | * Number of active (unexpired) challenges. | |
| 71 | */ | |
| 72 | get size () { | |
| 73 | return this._active.size | |
| 74 | } | |
| 75 | ||
| 76 | _cleanup () { | |
| 77 | const now = Date.now() | |
| 78 | for (const [challenge, expiresAt] of this._active) { | |
| 79 | if (expiresAt < now) this._active.delete(challenge) | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Stop the cleanup timer. Call when shutting down. | |
| 85 | */ | |
| 86 | destroy () { | |
| 87 | if (this._cleanupTimer) { | |
| 88 | clearInterval(this._cleanupTimer) | |
| 89 | this._cleanupTimer = null | |
| 90 | } | |
| 91 | this._active.clear() | |
| 92 | } | |
| 93 | } |
| … | … | @@ -0,0 +1,951 @@ |
| 1 | { | |
| 2 | "name": "anproto", | |
| 3 | "version": "0.1.0", | |
| 4 | "lockfileVersion": 3, | |
| 5 | "requires": true, | |
| 6 | "packages": { | |
| 7 | "": { | |
| 8 | "name": "anproto", | |
| 9 | "version": "0.1.0", | |
| 10 | "dependencies": { | |
| 11 | "b4a": "^1.6.0", | |
| 12 | "hypercore-crypto": "^3.4.0", | |
| 13 | "sodium-native": "^4.0.0" | |
| 14 | }, | |
| 15 | "devDependencies": { | |
| 16 | "brittle": "^3.0.0" | |
| 17 | } | |
| 18 | }, | |
| 19 | "node_modules/@jridgewell/resolve-uri": { | |
| 20 | "version": "3.1.2", | |
| 21 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", | |
| 22 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", | |
| 23 | "dev": true, | |
| 24 | "license": "MIT", | |
| 25 | "engines": { | |
| 26 | "node": ">=6.0.0" | |
| 27 | } | |
| 28 | }, | |
| 29 | "node_modules/@jridgewell/sourcemap-codec": { | |
| 30 | "version": "1.5.5", | |
| 31 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", | |
| 32 | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", | |
| 33 | "dev": true, | |
| 34 | "license": "MIT" | |
| 35 | }, | |
| 36 | "node_modules/@jridgewell/trace-mapping": { | |
| 37 | "version": "0.3.31", | |
| 38 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", | |
| 39 | "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", | |
| 40 | "dev": true, | |
| 41 | "license": "MIT", | |
| 42 | "dependencies": { | |
| 43 | "@jridgewell/resolve-uri": "^3.1.0", | |
| 44 | "@jridgewell/sourcemap-codec": "^1.4.14" | |
| 45 | } | |
| 46 | }, | |
| 47 | "node_modules/@types/istanbul-lib-coverage": { | |
| 48 | "version": "2.0.6", | |
| 49 | "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", | |
| 50 | "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", | |
| 51 | "dev": true, | |
| 52 | "license": "MIT" | |
| 53 | }, | |
| 54 | "node_modules/b4a": { | |
| 55 | "version": "1.8.0", | |
| 56 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", | |
| 57 | "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", | |
| 58 | "license": "Apache-2.0", | |
| 59 | "peerDependencies": { | |
| 60 | "react-native-b4a": "*" | |
| 61 | }, | |
| 62 | "peerDependenciesMeta": { | |
| 63 | "react-native-b4a": { | |
| 64 | "optional": true | |
| 65 | } | |
| 66 | } | |
| 67 | }, | |
| 68 | "node_modules/bare-abort": { | |
| 69 | "version": "2.0.13", | |
| 70 | "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz", | |
| 71 | "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==", | |
| 72 | "dev": true, | |
| 73 | "license": "Apache-2.0" | |
| 74 | }, | |
| 75 | "node_modules/bare-addon-resolve": { | |
| 76 | "version": "1.10.0", | |
| 77 | "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz", | |
| 78 | "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==", | |
| 79 | "license": "Apache-2.0", | |
| 80 | "dependencies": { | |
| 81 | "bare-module-resolve": "^1.10.0", | |
| 82 | "bare-semver": "^1.0.0" | |
| 83 | }, | |
| 84 | "peerDependencies": { | |
| 85 | "bare-url": "*" | |
| 86 | }, | |
| 87 | "peerDependenciesMeta": { | |
| 88 | "bare-url": { | |
| 89 | "optional": true | |
| 90 | } | |
| 91 | } | |
| 92 | }, | |
| 93 | "node_modules/bare-ansi-escapes": { | |
| 94 | "version": "2.2.3", | |
| 95 | "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz", | |
| 96 | "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==", | |
| 97 | "license": "Apache-2.0", | |
| 98 | "dependencies": { | |
| 99 | "bare-stream": "^2.6.5" | |
| 100 | }, | |
| 101 | "peerDependencies": { | |
| 102 | "bare-buffer": "*" | |
| 103 | }, | |
| 104 | "peerDependenciesMeta": { | |
| 105 | "bare-buffer": { | |
| 106 | "optional": true | |
| 107 | } | |
| 108 | } | |
| 109 | }, | |
| 110 | "node_modules/bare-assert": { | |
| 111 | "version": "1.2.0", | |
| 112 | "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz", | |
| 113 | "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==", | |
| 114 | "license": "Apache-2.0", | |
| 115 | "dependencies": { | |
| 116 | "bare-inspect": "^3.1.2" | |
| 117 | } | |
| 118 | }, | |
| 119 | "node_modules/bare-bundle": { | |
| 120 | "version": "1.10.0", | |
| 121 | "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz", | |
| 122 | "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==", | |
| 123 | "dev": true, | |
| 124 | "license": "Apache-2.0", | |
| 125 | "peerDependencies": { | |
| 126 | "bare-buffer": "*", | |
| 127 | "bare-url": "*" | |
| 128 | }, | |
| 129 | "peerDependenciesMeta": { | |
| 130 | "bare-buffer": { | |
| 131 | "optional": true | |
| 132 | }, | |
| 133 | "bare-url": { | |
| 134 | "optional": true | |
| 135 | } | |
| 136 | } | |
| 137 | }, | |
| 138 | "node_modules/bare-cov": { | |
| 139 | "version": "1.2.1", | |
| 140 | "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz", | |
| 141 | "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==", | |
| 142 | "dev": true, | |
| 143 | "license": "Apache-2.0", | |
| 144 | "dependencies": { | |
| 145 | "bare-fs": "^4.1.2", | |
| 146 | "bare-inspector": "^4.0.1", | |
| 147 | "bare-path": "^3.0.0", | |
| 148 | "bare-process": "^4.2.1", | |
| 149 | "bare-url": "^2.1.5", | |
| 150 | "bare-v8-to-istanbul": "^1.0.2", | |
| 151 | "picomatch": "^4.0.2", | |
| 152 | "which-runtime": "^1.2.1" | |
| 153 | } | |
| 154 | }, | |
| 155 | "node_modules/bare-crypto": { | |
| 156 | "version": "1.13.4", | |
| 157 | "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz", | |
| 158 | "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==", | |
| 159 | "dev": true, | |
| 160 | "license": "Apache-2.0", | |
| 161 | "dependencies": { | |
| 162 | "bare-assert": "^1.2.0", | |
| 163 | "bare-stream": "^2.6.3" | |
| 164 | }, | |
| 165 | "peerDependencies": { | |
| 166 | "bare-buffer": "*" | |
| 167 | }, | |
| 168 | "peerDependenciesMeta": { | |
| 169 | "bare-buffer": { | |
| 170 | "optional": true | |
| 171 | } | |
| 172 | } | |
| 173 | }, | |
| 174 | "node_modules/bare-debug-log": { | |
| 175 | "version": "2.0.0", | |
| 176 | "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz", | |
| 177 | "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==", | |
| 178 | "dev": true, | |
| 179 | "license": "Apache-2.0", | |
| 180 | "dependencies": { | |
| 181 | "bare-os": "^3.0.1" | |
| 182 | } | |
| 183 | }, | |
| 184 | "node_modules/bare-dns": { | |
| 185 | "version": "2.1.4", | |
| 186 | "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz", | |
| 187 | "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==", | |
| 188 | "dev": true, | |
| 189 | "license": "Apache-2.0", | |
| 190 | "engines": { | |
| 191 | "bare": ">=1.7.0" | |
| 192 | } | |
| 193 | }, | |
| 194 | "node_modules/bare-encoding": { | |
| 195 | "version": "1.0.3", | |
| 196 | "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz", | |
| 197 | "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==", | |
| 198 | "dev": true, | |
| 199 | "license": "Apache-2.0", | |
| 200 | "peerDependencies": { | |
| 201 | "bare-buffer": "*" | |
| 202 | }, | |
| 203 | "peerDependenciesMeta": { | |
| 204 | "bare-buffer": { | |
| 205 | "optional": true | |
| 206 | } | |
| 207 | } | |
| 208 | }, | |
| 209 | "node_modules/bare-env": { | |
| 210 | "version": "3.0.0", | |
| 211 | "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz", | |
| 212 | "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==", | |
| 213 | "dev": true, | |
| 214 | "license": "Apache-2.0", | |
| 215 | "dependencies": { | |
| 216 | "bare-os": "^3.0.1" | |
| 217 | } | |
| 218 | }, | |
| 219 | "node_modules/bare-events": { | |
| 220 | "version": "2.8.2", | |
| 221 | "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", | |
| 222 | "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", | |
| 223 | "license": "Apache-2.0", | |
| 224 | "peerDependencies": { | |
| 225 | "bare-abort-controller": "*" | |
| 226 | }, | |
| 227 | "peerDependenciesMeta": { | |
| 228 | "bare-abort-controller": { | |
| 229 | "optional": true | |
| 230 | } | |
| 231 | } | |
| 232 | }, | |
| 233 | "node_modules/bare-format": { | |
| 234 | "version": "1.0.2", | |
| 235 | "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz", | |
| 236 | "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==", | |
| 237 | "dev": true, | |
| 238 | "license": "Apache-2.0", | |
| 239 | "dependencies": { | |
| 240 | "bare-inspect": "^3.0.0" | |
| 241 | } | |
| 242 | }, | |
| 243 | "node_modules/bare-fs": { | |
| 244 | "version": "4.7.0", | |
| 245 | "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.0.tgz", | |
| 246 | "integrity": "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==", | |
| 247 | "dev": true, | |
| 248 | "license": "Apache-2.0", | |
| 249 | "dependencies": { | |
| 250 | "bare-events": "^2.5.4", | |
| 251 | "bare-path": "^3.0.0", | |
| 252 | "bare-stream": "^2.6.4", | |
| 253 | "bare-url": "^2.2.2", | |
| 254 | "fast-fifo": "^1.3.2" | |
| 255 | }, | |
| 256 | "engines": { | |
| 257 | "bare": ">=1.16.0" | |
| 258 | }, | |
| 259 | "peerDependencies": { | |
| 260 | "bare-buffer": "*" | |
| 261 | }, | |
| 262 | "peerDependenciesMeta": { | |
| 263 | "bare-buffer": { | |
| 264 | "optional": true | |
| 265 | } | |
| 266 | } | |
| 267 | }, | |
| 268 | "node_modules/bare-hrtime": { | |
| 269 | "version": "2.1.1", | |
| 270 | "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz", | |
| 271 | "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==", | |
| 272 | "dev": true, | |
| 273 | "license": "Apache-2.0" | |
| 274 | }, | |
| 275 | "node_modules/bare-http-parser": { | |
| 276 | "version": "1.1.3", | |
| 277 | "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz", | |
| 278 | "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==", | |
| 279 | "dev": true, | |
| 280 | "license": "Apache-2.0" | |
| 281 | }, | |
| 282 | "node_modules/bare-http1": { | |
| 283 | "version": "4.5.6", | |
| 284 | "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz", | |
| 285 | "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==", | |
| 286 | "dev": true, | |
| 287 | "license": "Apache-2.0", | |
| 288 | "dependencies": { | |
| 289 | "bare-events": "^2.6.0", | |
| 290 | "bare-http-parser": "^1.1.1", | |
| 291 | "bare-stream": "^2.10.0", | |
| 292 | "bare-tcp": "^2.2.0" | |
| 293 | }, | |
| 294 | "peerDependencies": { | |
| 295 | "bare-buffer": "*", | |
| 296 | "bare-url": "*" | |
| 297 | }, | |
| 298 | "peerDependenciesMeta": { | |
| 299 | "bare-buffer": { | |
| 300 | "optional": true | |
| 301 | }, | |
| 302 | "bare-url": { | |
| 303 | "optional": true | |
| 304 | } | |
| 305 | } | |
| 306 | }, | |
| 307 | "node_modules/bare-https": { | |
| 308 | "version": "2.1.3", | |
| 309 | "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz", | |
| 310 | "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==", | |
| 311 | "dev": true, | |
| 312 | "license": "Apache-2.0", | |
| 313 | "dependencies": { | |
| 314 | "bare-http1": "^4.4.0", | |
| 315 | "bare-tcp": "^2.2.0", | |
| 316 | "bare-tls": "^2.0.0" | |
| 317 | } | |
| 318 | }, | |
| 319 | "node_modules/bare-inspect": { | |
| 320 | "version": "3.1.4", | |
| 321 | "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz", | |
| 322 | "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==", | |
| 323 | "license": "Apache-2.0", | |
| 324 | "dependencies": { | |
| 325 | "bare-ansi-escapes": "^2.1.0", | |
| 326 | "bare-type": "^1.0.0" | |
| 327 | }, | |
| 328 | "engines": { | |
| 329 | "bare": ">=1.18.0" | |
| 330 | } | |
| 331 | }, | |
| 332 | "node_modules/bare-inspector": { | |
| 333 | "version": "4.0.3", | |
| 334 | "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz", | |
| 335 | "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==", | |
| 336 | "dev": true, | |
| 337 | "license": "Apache-2.0", | |
| 338 | "dependencies": { | |
| 339 | "bare-events": "^2.1.0", | |
| 340 | "bare-http1": "^4.0.0", | |
| 341 | "bare-stream": "^2.0.0", | |
| 342 | "bare-url": "^2.0.0", | |
| 343 | "bare-ws": "^2.0.0" | |
| 344 | }, | |
| 345 | "engines": { | |
| 346 | "bare": ">=1.2.0" | |
| 347 | }, | |
| 348 | "peerDependencies": { | |
| 349 | "bare-tcp": "*" | |
| 350 | }, | |
| 351 | "peerDependenciesMeta": { | |
| 352 | "bare-tcp": { | |
| 353 | "optional": true | |
| 354 | } | |
| 355 | } | |
| 356 | }, | |
| 357 | "node_modules/bare-module": { | |
| 358 | "version": "6.1.3", | |
| 359 | "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz", | |
| 360 | "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==", | |
| 361 | "dev": true, | |
| 362 | "license": "Apache-2.0", | |
| 363 | "dependencies": { | |
| 364 | "bare-bundle": "^1.3.0", | |
| 365 | "bare-module-lexer": "^1.0.0", | |
| 366 | "bare-module-resolve": "^1.8.0", | |
| 367 | "bare-path": "^3.0.0", | |
| 368 | "bare-url": "^2.0.1" | |
| 369 | }, | |
| 370 | "engines": { | |
| 371 | "bare": ">=1.23.0" | |
| 372 | }, | |
| 373 | "peerDependencies": { | |
| 374 | "bare-buffer": "*" | |
| 375 | }, | |
| 376 | "peerDependenciesMeta": { | |
| 377 | "bare-buffer": { | |
| 378 | "optional": true | |
| 379 | } | |
| 380 | } | |
| 381 | }, | |
| 382 | "node_modules/bare-module-lexer": { | |
| 383 | "version": "1.4.7", | |
| 384 | "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz", | |
| 385 | "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==", | |
| 386 | "dev": true, | |
| 387 | "license": "Apache-2.0", | |
| 388 | "dependencies": { | |
| 389 | "require-addon": "^1.0.2" | |
| 390 | }, | |
| 391 | "peerDependencies": { | |
| 392 | "bare-buffer": "*" | |
| 393 | }, | |
| 394 | "peerDependenciesMeta": { | |
| 395 | "bare-buffer": { | |
| 396 | "optional": true | |
| 397 | } | |
| 398 | } | |
| 399 | }, | |
| 400 | "node_modules/bare-module-resolve": { | |
| 401 | "version": "1.12.1", | |
| 402 | "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", | |
| 403 | "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==", | |
| 404 | "license": "Apache-2.0", | |
| 405 | "dependencies": { | |
| 406 | "bare-semver": "^1.0.0" | |
| 407 | }, | |
| 408 | "peerDependencies": { | |
| 409 | "bare-url": "*" | |
| 410 | }, | |
| 411 | "peerDependenciesMeta": { | |
| 412 | "bare-url": { | |
| 413 | "optional": true | |
| 414 | } | |
| 415 | } | |
| 416 | }, | |
| 417 | "node_modules/bare-net": { | |
| 418 | "version": "2.3.1", | |
| 419 | "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz", | |
| 420 | "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==", | |
| 421 | "dev": true, | |
| 422 | "license": "Apache-2.0", | |
| 423 | "dependencies": { | |
| 424 | "bare-events": "^2.2.2", | |
| 425 | "bare-pipe": "^4.0.0", | |
| 426 | "bare-stream": "^2.0.0", | |
| 427 | "bare-tcp": "^2.0.0" | |
| 428 | } | |
| 429 | }, | |
| 430 | "node_modules/bare-os": { | |
| 431 | "version": "3.8.7", | |
| 432 | "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", | |
| 433 | "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", | |
| 434 | "devOptional": true, | |
| 435 | "license": "Apache-2.0", | |
| 436 | "engines": { | |
| 437 | "bare": ">=1.14.0" | |
| 438 | } | |
| 439 | }, | |
| 440 | "node_modules/bare-path": { | |
| 441 | "version": "3.0.0", | |
| 442 | "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", | |
| 443 | "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", | |
| 444 | "devOptional": true, | |
| 445 | "license": "Apache-2.0", | |
| 446 | "dependencies": { | |
| 447 | "bare-os": "^3.0.1" | |
| 448 | } | |
| 449 | }, | |
| 450 | "node_modules/bare-pipe": { | |
| 451 | "version": "4.1.5", | |
| 452 | "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz", | |
| 453 | "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==", | |
| 454 | "dev": true, | |
| 455 | "license": "Apache-2.0", | |
| 456 | "dependencies": { | |
| 457 | "bare-events": "^2.0.0", | |
| 458 | "bare-stream": "^2.0.0" | |
| 459 | }, | |
| 460 | "engines": { | |
| 461 | "bare": ">=1.16.0" | |
| 462 | } | |
| 463 | }, | |
| 464 | "node_modules/bare-process": { | |
| 465 | "version": "4.4.1", | |
| 466 | "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz", | |
| 467 | "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==", | |
| 468 | "dev": true, | |
| 469 | "license": "Apache-2.0", | |
| 470 | "dependencies": { | |
| 471 | "bare-abort": "^2.0.13", | |
| 472 | "bare-env": "^3.0.0", | |
| 473 | "bare-events": "^2.3.1", | |
| 474 | "bare-hrtime": "^2.0.0", | |
| 475 | "bare-os": "^3.7.1", | |
| 476 | "bare-signals": "^4.0.0", | |
| 477 | "bare-stdio": "^1.0.1" | |
| 478 | } | |
| 479 | }, | |
| 480 | "node_modules/bare-semver": { | |
| 481 | "version": "1.0.3", | |
| 482 | "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz", | |
| 483 | "integrity": "sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==", | |
| 484 | "license": "Apache-2.0" | |
| 485 | }, | |
| 486 | "node_modules/bare-signals": { | |
| 487 | "version": "4.2.0", | |
| 488 | "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz", | |
| 489 | "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==", | |
| 490 | "dev": true, | |
| 491 | "license": "Apache-2.0", | |
| 492 | "dependencies": { | |
| 493 | "bare-events": "^2.5.3", | |
| 494 | "bare-os": "^3.3.1" | |
| 495 | }, | |
| 496 | "engines": { | |
| 497 | "bare": ">=1.7.0" | |
| 498 | } | |
| 499 | }, | |
| 500 | "node_modules/bare-stdio": { | |
| 501 | "version": "1.0.2", | |
| 502 | "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz", | |
| 503 | "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==", | |
| 504 | "dev": true, | |
| 505 | "license": "Apache-2.0", | |
| 506 | "dependencies": { | |
| 507 | "bare-fs": "^4.5.2", | |
| 508 | "bare-pipe": "^4.1.5", | |
| 509 | "bare-tty": "^5.0.3" | |
| 510 | } | |
| 511 | }, | |
| 512 | "node_modules/bare-stream": { | |
| 513 | "version": "2.13.0", | |
| 514 | "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", | |
| 515 | "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", | |
| 516 | "license": "Apache-2.0", | |
| 517 | "dependencies": { | |
| 518 | "streamx": "^2.25.0", | |
| 519 | "teex": "^1.0.1" | |
| 520 | }, | |
| 521 | "peerDependencies": { | |
| 522 | "bare-abort-controller": "*", | |
| 523 | "bare-buffer": "*", | |
| 524 | "bare-events": "*" | |
| 525 | }, | |
| 526 | "peerDependenciesMeta": { | |
| 527 | "bare-abort-controller": { | |
| 528 | "optional": true | |
| 529 | }, | |
| 530 | "bare-buffer": { | |
| 531 | "optional": true | |
| 532 | }, | |
| 533 | "bare-events": { | |
| 534 | "optional": true | |
| 535 | } | |
| 536 | } | |
| 537 | }, | |
| 538 | "node_modules/bare-stylize": { | |
| 539 | "version": "0.0.1", | |
| 540 | "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz", | |
| 541 | "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==", | |
| 542 | "dev": true, | |
| 543 | "license": "Apache-2.0", | |
| 544 | "dependencies": { | |
| 545 | "bare-ansi-escapes": "^2.2.3", | |
| 546 | "bare-process": "^4.2.1" | |
| 547 | } | |
| 548 | }, | |
| 549 | "node_modules/bare-subprocess": { | |
| 550 | "version": "5.2.3", | |
| 551 | "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz", | |
| 552 | "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==", | |
| 553 | "dev": true, | |
| 554 | "license": "Apache-2.0", | |
| 555 | "dependencies": { | |
| 556 | "bare-env": "^3.0.0", | |
| 557 | "bare-events": "^2.5.4", | |
| 558 | "bare-os": "^3.0.1", | |
| 559 | "bare-pipe": "^4.0.0", | |
| 560 | "bare-url": "^2.2.2" | |
| 561 | }, | |
| 562 | "engines": { | |
| 563 | "bare": ">=1.7.0" | |
| 564 | }, | |
| 565 | "peerDependencies": { | |
| 566 | "bare-buffer": "*" | |
| 567 | }, | |
| 568 | "peerDependenciesMeta": { | |
| 569 | "bare-buffer": { | |
| 570 | "optional": true | |
| 571 | } | |
| 572 | } | |
| 573 | }, | |
| 574 | "node_modules/bare-tcp": { | |
| 575 | "version": "2.2.7", | |
| 576 | "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz", | |
| 577 | "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==", | |
| 578 | "dev": true, | |
| 579 | "license": "Apache-2.0", | |
| 580 | "dependencies": { | |
| 581 | "bare-dns": "^2.0.4", | |
| 582 | "bare-events": "^2.5.4", | |
| 583 | "bare-stream": "^2.6.4" | |
| 584 | }, | |
| 585 | "engines": { | |
| 586 | "bare": ">=1.16.0" | |
| 587 | } | |
| 588 | }, | |
| 589 | "node_modules/bare-tls": { | |
| 590 | "version": "2.2.3", | |
| 591 | "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.3.tgz", | |
| 592 | "integrity": "sha512-dPYBGEXtgLceRFMfGaF2/rqR86/xMxMyrM9ootO/gaRKL/z2uNHJs7aP7IOBtnJF8eUCt5qwMzbKfrKgDIxPLg==", | |
| 593 | "dev": true, | |
| 594 | "license": "Apache-2.0", | |
| 595 | "dependencies": { | |
| 596 | "bare-net": "^2.0.1", | |
| 597 | "bare-stream": "^2.6.4" | |
| 598 | }, | |
| 599 | "engines": { | |
| 600 | "bare": ">=1.7.0" | |
| 601 | } | |
| 602 | }, | |
| 603 | "node_modules/bare-tty": { | |
| 604 | "version": "5.1.0", | |
| 605 | "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz", | |
| 606 | "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==", | |
| 607 | "dev": true, | |
| 608 | "license": "Apache-2.0", | |
| 609 | "dependencies": { | |
| 610 | "bare-events": "^2.2.0", | |
| 611 | "bare-signals": "^4.0.0", | |
| 612 | "bare-stream": "^2.0.0" | |
| 613 | }, | |
| 614 | "engines": { | |
| 615 | "bare": ">=1.16.0" | |
| 616 | } | |
| 617 | }, | |
| 618 | "node_modules/bare-type": { | |
| 619 | "version": "1.1.0", | |
| 620 | "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz", | |
| 621 | "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==", | |
| 622 | "license": "Apache-2.0", | |
| 623 | "engines": { | |
| 624 | "bare": ">=1.2.0" | |
| 625 | } | |
| 626 | }, | |
| 627 | "node_modules/bare-url": { | |
| 628 | "version": "2.4.0", | |
| 629 | "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", | |
| 630 | "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", | |
| 631 | "devOptional": true, | |
| 632 | "license": "Apache-2.0", | |
| 633 | "peer": true, | |
| 634 | "dependencies": { | |
| 635 | "bare-path": "^3.0.0" | |
| 636 | } | |
| 637 | }, | |
| 638 | "node_modules/bare-utils": { | |
| 639 | "version": "1.6.0", | |
| 640 | "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz", | |
| 641 | "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==", | |
| 642 | "dev": true, | |
| 643 | "license": "Apache-2.0", | |
| 644 | "dependencies": { | |
| 645 | "bare-debug-log": "^2.0.0", | |
| 646 | "bare-encoding": "^1.0.0", | |
| 647 | "bare-format": "^1.0.0", | |
| 648 | "bare-inspect": "^3.0.0", | |
| 649 | "bare-stylize": "^0.0.1", | |
| 650 | "bare-type": "^1.0.6" | |
| 651 | } | |
| 652 | }, | |
| 653 | "node_modules/bare-v8-to-istanbul": { | |
| 654 | "version": "1.0.2", | |
| 655 | "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz", | |
| 656 | "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==", | |
| 657 | "dev": true, | |
| 658 | "license": "Apache-2.0", | |
| 659 | "dependencies": { | |
| 660 | "bare-assert": "^1.0.2", | |
| 661 | "bare-fs": "^4.1.2", | |
| 662 | "bare-module": "^6.1.2", | |
| 663 | "bare-path": "^3.0.0", | |
| 664 | "bare-process": "^4.2.0", | |
| 665 | "bare-url": "^2.1.5", | |
| 666 | "bare-utils": "^1.2.0", | |
| 667 | "v8-to-istanbul": "^9.3.0", | |
| 668 | "which-runtime": "^1.2.1" | |
| 669 | } | |
| 670 | }, | |
| 671 | "node_modules/bare-ws": { | |
| 672 | "version": "2.1.0", | |
| 673 | "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz", | |
| 674 | "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==", | |
| 675 | "dev": true, | |
| 676 | "license": "Apache-2.0", | |
| 677 | "dependencies": { | |
| 678 | "bare-crypto": "^1.2.0", | |
| 679 | "bare-events": "^2.3.1", | |
| 680 | "bare-http1": "^4.0.0", | |
| 681 | "bare-https": "^2.0.0", | |
| 682 | "bare-stream": "^2.1.2" | |
| 683 | }, | |
| 684 | "peerDependencies": { | |
| 685 | "bare-buffer": "*", | |
| 686 | "bare-url": "*" | |
| 687 | }, | |
| 688 | "peerDependenciesMeta": { | |
| 689 | "bare-buffer": { | |
| 690 | "optional": true | |
| 691 | }, | |
| 692 | "bare-url": { | |
| 693 | "optional": true | |
| 694 | } | |
| 695 | } | |
| 696 | }, | |
| 697 | "node_modules/brittle": { | |
| 698 | "version": "3.19.1", | |
| 699 | "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz", | |
| 700 | "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==", | |
| 701 | "dev": true, | |
| 702 | "license": "Apache-2.0", | |
| 703 | "dependencies": { | |
| 704 | "b4a": "^1.6.0", | |
| 705 | "bare-assert": "^1.0.2", | |
| 706 | "bare-cov": "^1.1.0", | |
| 707 | "bare-fs": "^4.1.6", | |
| 708 | "bare-os": "^3.6.1", | |
| 709 | "bare-path": "^3.0.0", | |
| 710 | "bare-process": "^4.2.1", | |
| 711 | "bare-subprocess": "^5.0.0", | |
| 712 | "bare-url": "^2.1.6", | |
| 713 | "error-stack-parser": "^2.1.4", | |
| 714 | "globbie": "^1.0.2", | |
| 715 | "paparam": "^1.6.2", | |
| 716 | "same-object": "^1.0.2", | |
| 717 | "test-tmp": "^1.4.0", | |
| 718 | "tmatch": "^5.0.0" | |
| 719 | }, | |
| 720 | "bin": { | |
| 721 | "brittle": "bin/node.js", | |
| 722 | "brittle-bare": "bin/bare.js", | |
| 723 | "brittle-node": "bin/node.js", | |
| 724 | "brittle-pear": "bin/pear.js" | |
| 725 | } | |
| 726 | }, | |
| 727 | "node_modules/compact-encoding": { | |
| 728 | "version": "2.19.2", | |
| 729 | "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz", | |
| 730 | "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==", | |
| 731 | "license": "Apache-2.0", | |
| 732 | "dependencies": { | |
| 733 | "b4a": "^1.3.0" | |
| 734 | } | |
| 735 | }, | |
| 736 | "node_modules/convert-source-map": { | |
| 737 | "version": "2.0.0", | |
| 738 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", | |
| 739 | "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", | |
| 740 | "dev": true, | |
| 741 | "license": "MIT" | |
| 742 | }, | |
| 743 | "node_modules/error-stack-parser": { | |
| 744 | "version": "2.1.4", | |
| 745 | "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", | |
| 746 | "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", | |
| 747 | "dev": true, | |
| 748 | "license": "MIT", | |
| 749 | "dependencies": { | |
| 750 | "stackframe": "^1.3.4" | |
| 751 | } | |
| 752 | }, | |
| 753 | "node_modules/events-universal": { | |
| 754 | "version": "1.0.1", | |
| 755 | "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", | |
| 756 | "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", | |
| 757 | "license": "Apache-2.0", | |
| 758 | "dependencies": { | |
| 759 | "bare-events": "^2.7.0" | |
| 760 | } | |
| 761 | }, | |
| 762 | "node_modules/fast-fifo": { | |
| 763 | "version": "1.3.2", | |
| 764 | "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", | |
| 765 | "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", | |
| 766 | "license": "MIT" | |
| 767 | }, | |
| 768 | "node_modules/globbie": { | |
| 769 | "version": "1.0.3", | |
| 770 | "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz", | |
| 771 | "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==", | |
| 772 | "dev": true, | |
| 773 | "license": "Apache-2.0", | |
| 774 | "dependencies": { | |
| 775 | "bare-fs": "^4.1.2", | |
| 776 | "bare-path": "^3.0.0", | |
| 777 | "bare-process": "^4.2.1", | |
| 778 | "picomatch": "^4.0.2" | |
| 779 | } | |
| 780 | }, | |
| 781 | "node_modules/hypercore-crypto": { | |
| 782 | "version": "3.6.1", | |
| 783 | "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz", | |
| 784 | "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==", | |
| 785 | "license": "MIT", | |
| 786 | "dependencies": { | |
| 787 | "b4a": "^1.6.6", | |
| 788 | "compact-encoding": "^2.15.0", | |
| 789 | "sodium-universal": "^5.0.0" | |
| 790 | } | |
| 791 | }, | |
| 792 | "node_modules/paparam": { | |
| 793 | "version": "1.10.1", | |
| 794 | "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz", | |
| 795 | "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==", | |
| 796 | "dev": true, | |
| 797 | "license": "Apache-2.0" | |
| 798 | }, | |
| 799 | "node_modules/picomatch": { | |
| 800 | "version": "4.0.4", | |
| 801 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", | |
| 802 | "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", | |
| 803 | "dev": true, | |
| 804 | "license": "MIT", | |
| 805 | "engines": { | |
| 806 | "node": ">=12" | |
| 807 | }, | |
| 808 | "funding": { | |
| 809 | "url": "https://github.com/sponsors/jonschlinkert" | |
| 810 | } | |
| 811 | }, | |
| 812 | "node_modules/require-addon": { | |
| 813 | "version": "1.2.0", | |
| 814 | "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", | |
| 815 | "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", | |
| 816 | "license": "Apache-2.0", | |
| 817 | "dependencies": { | |
| 818 | "bare-addon-resolve": "^1.3.0" | |
| 819 | }, | |
| 820 | "engines": { | |
| 821 | "bare": ">=1.10.0" | |
| 822 | } | |
| 823 | }, | |
| 824 | "node_modules/same-object": { | |
| 825 | "version": "1.0.2", | |
| 826 | "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz", | |
| 827 | "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==", | |
| 828 | "dev": true, | |
| 829 | "license": "MIT" | |
| 830 | }, | |
| 831 | "node_modules/sodium-native": { | |
| 832 | "version": "4.3.3", | |
| 833 | "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", | |
| 834 | "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", | |
| 835 | "license": "MIT", | |
| 836 | "dependencies": { | |
| 837 | "require-addon": "^1.1.0" | |
| 838 | } | |
| 839 | }, | |
| 840 | "node_modules/sodium-universal": { | |
| 841 | "version": "5.0.1", | |
| 842 | "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz", | |
| 843 | "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==", | |
| 844 | "license": "MIT", | |
| 845 | "dependencies": { | |
| 846 | "sodium-native": "^5.0.1" | |
| 847 | }, | |
| 848 | "peerDependencies": { | |
| 849 | "sodium-javascript": "~0.8.0" | |
| 850 | }, | |
| 851 | "peerDependenciesMeta": { | |
| 852 | "sodium-javascript": { | |
| 853 | "optional": true | |
| 854 | } | |
| 855 | } | |
| 856 | }, | |
| 857 | "node_modules/sodium-universal/node_modules/sodium-native": { | |
| 858 | "version": "5.1.0", | |
| 859 | "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz", | |
| 860 | "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==", | |
| 861 | "license": "MIT", | |
| 862 | "dependencies": { | |
| 863 | "bare-assert": "^1.2.0", | |
| 864 | "require-addon": "^1.1.0", | |
| 865 | "which-runtime": "^1.2.1" | |
| 866 | }, | |
| 867 | "engines": { | |
| 868 | "bare": ">=1.16.0" | |
| 869 | } | |
| 870 | }, | |
| 871 | "node_modules/stackframe": { | |
| 872 | "version": "1.3.4", | |
| 873 | "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", | |
| 874 | "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", | |
| 875 | "dev": true, | |
| 876 | "license": "MIT" | |
| 877 | }, | |
| 878 | "node_modules/streamx": { | |
| 879 | "version": "2.25.0", | |
| 880 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", | |
| 881 | "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", | |
| 882 | "license": "MIT", | |
| 883 | "dependencies": { | |
| 884 | "events-universal": "^1.0.0", | |
| 885 | "fast-fifo": "^1.3.2", | |
| 886 | "text-decoder": "^1.1.0" | |
| 887 | } | |
| 888 | }, | |
| 889 | "node_modules/teex": { | |
| 890 | "version": "1.0.1", | |
| 891 | "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", | |
| 892 | "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", | |
| 893 | "license": "MIT", | |
| 894 | "dependencies": { | |
| 895 | "streamx": "^2.12.5" | |
| 896 | } | |
| 897 | }, | |
| 898 | "node_modules/test-tmp": { | |
| 899 | "version": "1.4.0", | |
| 900 | "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz", | |
| 901 | "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==", | |
| 902 | "dev": true, | |
| 903 | "license": "MIT", | |
| 904 | "dependencies": { | |
| 905 | "bare-fs": "^4.0.1", | |
| 906 | "bare-os": "^3.3.0", | |
| 907 | "bare-path": "^3.0.0" | |
| 908 | } | |
| 909 | }, | |
| 910 | "node_modules/text-decoder": { | |
| 911 | "version": "1.2.7", | |
| 912 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", | |
| 913 | "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", | |
| 914 | "license": "Apache-2.0", | |
| 915 | "dependencies": { | |
| 916 | "b4a": "^1.6.4" | |
| 917 | } | |
| 918 | }, | |
| 919 | "node_modules/tmatch": { | |
| 920 | "version": "5.0.0", | |
| 921 | "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz", | |
| 922 | "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==", | |
| 923 | "dev": true, | |
| 924 | "license": "ISC", | |
| 925 | "engines": { | |
| 926 | "node": ">=8" | |
| 927 | } | |
| 928 | }, | |
| 929 | "node_modules/v8-to-istanbul": { | |
| 930 | "version": "9.3.0", | |
| 931 | "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", | |
| 932 | "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", | |
| 933 | "dev": true, | |
| 934 | "license": "ISC", | |
| 935 | "dependencies": { | |
| 936 | "@jridgewell/trace-mapping": "^0.3.12", | |
| 937 | "@types/istanbul-lib-coverage": "^2.0.1", | |
| 938 | "convert-source-map": "^2.0.0" | |
| 939 | }, | |
| 940 | "engines": { | |
| 941 | "node": ">=10.12.0" | |
| 942 | } | |
| 943 | }, | |
| 944 | "node_modules/which-runtime": { | |
| 945 | "version": "1.3.2", | |
| 946 | "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz", | |
| 947 | "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", | |
| 948 | "license": "Apache-2.0" | |
| 949 | } | |
| 950 | } | |
| 951 | } |
| … | … | @@ -0,0 +1,18 @@ |
| 1 | { | |
| 2 | "name": "anproto", | |
| 3 | "version": "0.1.0", | |
| 4 | "description": "Wire-compatible implementation of evbogue's ANProto (Authenticated Non-networked protocol) — Ed25519 signed timestamp+hash messages", | |
| 5 | "type": "module", | |
| 6 | "main": "index.js", | |
| 7 | "scripts": { | |
| 8 | "test": "brittle test/*.test.js" | |
| 9 | }, | |
| 10 | "dependencies": { | |
| 11 | "b4a": "^1.6.0", | |
| 12 | "hypercore-crypto": "^3.4.0", | |
| 13 | "sodium-native": "^4.0.0" | |
| 14 | }, | |
| 15 | "devDependencies": { | |
| 16 | "brittle": "^3.0.0" | |
| 17 | } | |
| 18 | } |
| … | … | @@ -0,0 +1,150 @@ |
| 1 | import test from 'brittle' | |
| 2 | import { an, Challenges } from '../index.js' | |
| 3 | ||
| 4 | // ── Round-trip tests (functionality) ───────────────────────────────── | |
| 5 | ||
| 6 | test('an.gen() produces a 132-char keypair string', async (t) => { | |
| 7 | const k = await an.gen() | |
| 8 | t.is(typeof k, 'string') | |
| 9 | t.is(k.length, 132, 'keypair is 132 chars (44 pubkey + 88 secret, base64)') | |
| 10 | }) | |
| 11 | ||
| 12 | test('an.hash() produces deterministic 44-char base64 SHA-256', async (t) => { | |
| 13 | const h1 = await an.hash('Hello World') | |
| 14 | const h2 = await an.hash('Hello World') | |
| 15 | t.is(h1, h2, 'same input produces same hash') | |
| 16 | t.is(h1.length, 44, 'SHA-256 in base64 is 44 chars') | |
| 17 | ||
| 18 | // Wire compat: this is the canonical hash for "Hello World" | |
| 19 | // (verified against evbogue's reference implementation) | |
| 20 | t.is(h1, 'pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=', 'matches canonical anproto hash') | |
| 21 | }) | |
| 22 | ||
| 23 | test('an.sign() / an.open() round-trip', async (t) => { | |
| 24 | const k = await an.gen() | |
| 25 | const h = await an.hash('hello world') | |
| 26 | const sig = await an.sign(h, k) | |
| 27 | const opened = await an.open(sig) | |
| 28 | ||
| 29 | t.ok(opened.endsWith(h), 'opened message ends with the hash') | |
| 30 | t.is(opened.length, 13 + h.length, '13-digit timestamp followed by hash') | |
| 31 | }) | |
| 32 | ||
| 33 | test('an.sign() output starts with the public key', async (t) => { | |
| 34 | const k = await an.gen() | |
| 35 | const h = await an.hash('test') | |
| 36 | const sig = await an.sign(h, k) | |
| 37 | t.is(sig.substring(0, 44), an.publicKey(k), 'first 44 chars of sig are pubkey') | |
| 38 | }) | |
| 39 | ||
| 40 | test('an.open() rejects tampered signatures', async (t) => { | |
| 41 | const k = await an.gen() | |
| 42 | const h = await an.hash('test') | |
| 43 | const sig = await an.sign(h, k) | |
| 44 | // Flip a byte in the signature body | |
| 45 | const tampered = sig.substring(0, 50) + 'X' + sig.substring(51) | |
| 46 | await t.exception(an.open(tampered), /Invalid ANProto signature/, 'tampered sig rejected') | |
| 47 | }) | |
| 48 | ||
| 49 | test('an.open() rejects truncated messages', async (t) => { | |
| 50 | await t.exception(an.open('too-short'), /too short|invalid/i) | |
| 51 | }) | |
| 52 | ||
| 53 | test('an.parse() splits opened message into timestamp + hash', async (t) => { | |
| 54 | const k = await an.gen() | |
| 55 | const h = await an.hash('test data') | |
| 56 | const sig = await an.sign(h, k) | |
| 57 | const opened = await an.open(sig) | |
| 58 | const parsed = an.parse(opened) | |
| 59 | ||
| 60 | t.is(typeof parsed.timestamp, 'number') | |
| 61 | t.ok(parsed.timestamp > 1000000000000, 'timestamp is a valid millisecond epoch') | |
| 62 | t.is(parsed.hash, h, 'hash round-trips') | |
| 63 | }) | |
| 64 | ||
| 65 | // ── Wire compatibility with evbogue's canonical implementation ─────── | |
| 66 | // | |
| 67 | // Test vector generated using the canonical evbogue/ANProto with a fixed | |
| 68 | // keypair (derived from sha256("anproto-test-seed")) and a fixed message. | |
| 69 | // Ed25519 is deterministic, so the same key + same message ALWAYS produces | |
| 70 | // the same signature bytes — regardless of which library generates it. | |
| 71 | // | |
| 72 | // If our implementation produces different bytes, we are NOT wire-compatible. | |
| 73 | ||
| 74 | const VECTORS = { | |
| 75 | // Keypair derived from sha256("anproto-test-seed") via canonical impl | |
| 76 | pubkey: 'M8HUU6LqOD/vKavtsNJfPyY4Z5cyhVjzjViAD+FdeZo=', | |
| 77 | keypair: 'M8HUU6LqOD/vKavtsNJfPyY4Z5cyhVjzjViAD+FdeZo=Co+J2nXPQgNfn+S988mYWaEW7i3QBNdsvnpJtWOoCD4zwdRTouo4P+8pq+2w0l8/JjhnlzKFWPONWIAP4V15mg==', | |
| 78 | helloWorldHash: 'pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=', | |
| 79 | // Wire-format signature of the literal message "TEST_MESSAGE_FOR_WIRE_COMPAT" | |
| 80 | // produced by canonical evbogue ANProto with the keypair above. | |
| 81 | fixedMessage: 'TEST_MESSAGE_FOR_WIRE_COMPAT', | |
| 82 | fixedSignature: 'M8HUU6LqOD/vKavtsNJfPyY4Z5cyhVjzjViAD+FdeZo=Z2+ybxNj4K0JqIrOyV6cS829hH/qJhbtCd5wMgVD1FYnq//Z8WWnMbu1SXOvtAQGY4/DGwAado+WKjzt+ihOAlRFU1RfTUVTU0FHRV9GT1JfV0lSRV9DT01QQVQ=' | |
| 83 | } | |
| 84 | ||
| 85 | test('wire compat: hash("Hello World") matches canonical anproto', async (t) => { | |
| 86 | const h = await an.hash('Hello World') | |
| 87 | t.is(h, VECTORS.helloWorldHash) | |
| 88 | }) | |
| 89 | ||
| 90 | test('wire compat: open() decodes a signature produced by canonical anproto', async (t) => { | |
| 91 | // The fixedSignature was produced by evbogue's TweetNaCl-based impl. | |
| 92 | // Our open() must decode it byte-for-byte to be wire-compatible. | |
| 93 | const opened = await an.open(VECTORS.fixedSignature) | |
| 94 | t.is(opened, VECTORS.fixedMessage, 'our open() reads canonical signatures') | |
| 95 | }) | |
| 96 | ||
| 97 | test('wire compat: pubkey extraction matches canonical', async (t) => { | |
| 98 | // The first 44 chars of the keypair are the pubkey, in both impls. | |
| 99 | t.is(an.publicKey(VECTORS.keypair), VECTORS.pubkey) | |
| 100 | }) | |
| 101 | ||
| 102 | // ── Challenges (replay protection) ─────────────────────────────────── | |
| 103 | ||
| 104 | test('Challenges: generate + consume single-use', (t) => { | |
| 105 | const c = new Challenges({ ttl: 60000 }) | |
| 106 | const challenge = c.generate() | |
| 107 | ||
| 108 | t.is(typeof challenge, 'string') | |
| 109 | t.is(challenge.length, 44, 'random 32 bytes base64 = 44 chars') | |
| 110 | t.ok(c.has(challenge), 'challenge is active') | |
| 111 | t.is(c.size, 1) | |
| 112 | ||
| 113 | t.ok(c.consume(challenge), 'first consume succeeds') | |
| 114 | t.absent(c.consume(challenge), 'second consume fails (single-use)') | |
| 115 | t.absent(c.has(challenge), 'consumed challenge no longer active') | |
| 116 | t.is(c.size, 0) | |
| 117 | ||
| 118 | c.destroy() | |
| 119 | }) | |
| 120 | ||
| 121 | test('Challenges: unknown challenge rejected', (t) => { | |
| 122 | const c = new Challenges() | |
| 123 | t.absent(c.consume('not-a-real-challenge'), 'unknown challenge fails') | |
| 124 | t.absent(c.has('not-a-real-challenge')) | |
| 125 | c.destroy() | |
| 126 | }) | |
| 127 | ||
| 128 | test('Challenges: expired challenges rejected', async (t) => { | |
| 129 | const c = new Challenges({ ttl: 50, cleanupInterval: 10 }) | |
| 130 | const challenge = c.generate() | |
| 131 | await new Promise(r => setTimeout(r, 80)) | |
| 132 | t.absent(c.consume(challenge), 'expired challenge fails') | |
| 133 | c.destroy() | |
| 134 | }) | |
| 135 | ||
| 136 | test('Challenges: many challenges, only consumed ones removed', (t) => { | |
| 137 | const c = new Challenges() | |
| 138 | const ch1 = c.generate() | |
| 139 | const ch2 = c.generate() | |
| 140 | const ch3 = c.generate() | |
| 141 | ||
| 142 | t.is(c.size, 3) | |
| 143 | c.consume(ch2) | |
| 144 | t.is(c.size, 2) | |
| 145 | t.ok(c.has(ch1)) | |
| 146 | t.absent(c.has(ch2)) | |
| 147 | t.ok(c.has(ch3)) | |
| 148 | ||
| 149 | c.destroy() | |
| 150 | }) |