/**
* hyper-booking Identity — Browser ed25519 auth
*
* Generates ed25519 keypair CLIENT-SIDE using @noble/ed25519.
* The secret key never leaves the browser.
* Uses challenge-response signing for every authenticated request.
* Intercepts fetch() to inject auth headers automatically.
*
* Adapted from hypernote/pear-git identity.js.
* Uses vendored noble libs (zero CDN).
*/
window.HyperBookingIdentity = (function () {
var KEYPAIR_KEY = 'hb-keypair'
var PUBKEY_KEY = 'hb-pubkey'
var USERNAME_KEY = 'hb-username'
var cachedChallenge = null
var ed = null
var sha512 = null
var nobleLoading = null
/**
* Load @noble/ed25519 from vendored files
*/
function loadNoble () {
if (ed) return Promise.resolve()
if (nobleLoading) return nobleLoading
nobleLoading = (async function () {
try {
// Resolve vendor path from this script's src attribute
var scriptEl = document.querySelector('script[src*="identity.js"]')
var scriptSrc = scriptEl ? scriptEl.src : location.href
var vendorBase = scriptSrc.replace(/\/js\/identity\.js.*$/, '/vendor/')
var module = await import(vendorBase + 'noble-ed25519.js')
ed = module
var hashModule = await import(vendorBase + 'noble-sha512.js')
sha512 = hashModule.sha512
ed.etc.sha512Sync = function () {
return sha512.apply(null, arguments)
}
} catch (err) {
nobleLoading = null
console.error('[Identity] Failed to load noble ed25519:', err)
throw err
}
})()
return nobleLoading
}
function getKeypair () {
return localStorage.getItem(KEYPAIR_KEY) || null
}
function getPublicKey () {
return localStorage.getItem(PUBKEY_KEY) || null
}
function getUsername () {
return localStorage.getItem(USERNAME_KEY) || null
}
function setUsername (u) {
if (u) {
localStorage.setItem(USERNAME_KEY, u)
} else {
localStorage.removeItem(USERNAME_KEY)
}
}
async function generate () {
await loadNoble()
var seed = new Uint8Array(32)
crypto.getRandomValues(seed)
var publicKeyBytes = ed.getPublicKey(seed)
var secretKeyBytes = new Uint8Array(64)
secretKeyBytes.set(seed, 0)
secretKeyBytes.set(publicKeyBytes, 32)
var publicKeyB64 = bytesToBase64(publicKeyBytes)
var secretKeyB64 = bytesToBase64(secretKeyBytes)
var keypair = publicKeyB64 + secretKeyB64
localStorage.setItem(KEYPAIR_KEY, keypair)
localStorage.setItem(PUBKEY_KEY, publicKeyB64)
return { publicKey: publicKeyB64, keypair: keypair }
}
function getPathPrefix () {
var path = window.location.pathname
var idx = path.indexOf('/web/')
if (idx > 0) return path.substring(0, idx)
var dashIdx = path.indexOf('/dashboard')
if (dashIdx > 0) return path.substring(0, dashIdx)
var match = path.match(/^(\/[^/]+)\//)
if (match && match[1] !== '/api' && match[1] !== '/web') return match[1]
return ''
}
async function getChallenge () {
if (cachedChallenge) {
var remaining = cachedChallenge.expiresAt - Date.now()
if (remaining > 60000) return cachedChallenge
}
var prefix = getPathPrefix()
var res = await originalFetch(prefix + '/api/auth/challenge')
if (!res.ok) throw new Error('Failed to get challenge')
cachedChallenge = await res.json()
return cachedChallenge
}
async function signChallenge (challenge) {
await loadNoble()
var keypair = getKeypair()
if (!keypair) throw new Error('No keypair stored')
var secretKeyB64 = keypair.substring(44, 132)
var secretKey = base64ToBytes(secretKeyB64)
var seed = secretKey.slice(0, 32)
var challengeBytes = new TextEncoder().encode(challenge)
var signature = ed.sign(challengeBytes, seed)
return bytesToBase64(signature)
}
function base64ToBytes (b64) {
var bin = atob(b64)
var bytes = new Uint8Array(bin.length)
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
return bytes
}
function bytesToBase64 (bytes) {
var bin = ''
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i])
return btoa(bin)
}
function clear () {
localStorage.removeItem(KEYPAIR_KEY)
localStorage.removeItem(PUBKEY_KEY)
localStorage.removeItem(USERNAME_KEY)
cachedChallenge = null
}
function importKeypair (keypairStr) {
if (!keypairStr || keypairStr.length !== 132) {
throw new Error('Invalid keypair (must be 132 chars)')
}
var pubKey = keypairStr.substring(0, 44)
localStorage.setItem(KEYPAIR_KEY, keypairStr)
localStorage.setItem(PUBKEY_KEY, pubKey)
cachedChallenge = null
return pubKey
}
function exportKeypair () {
var keypair = getKeypair()
var pubKey = getPublicKey()
var username = getUsername()
if (!keypair) return null
return {
publicKey: pubKey,
keypair: keypair,
username: username,
exportedAt: new Date().toISOString()
}
}
// ==================== FETCH INTERCEPTION ====================
function shouldSkipAuth (url) {
if (url.indexOf('/api/') === -1) return true
if (url.indexOf('/api/auth/challenge') !== -1) return true
if (!getPublicKey()) return true
return false
}
var originalFetch = window.fetch
async function authenticatedFetch (input, init) {
var url = typeof input === 'string' ? input : input.url || ''
if (shouldSkipAuth(url)) {
return originalFetch(input, init)
}
var pubKey = getPublicKey()
if (!pubKey) {
return originalFetch(input, init)
}
try {
var ch = await getChallenge()
var signature = await signChallenge(ch.challenge)
init = init || {}
init.headers = init.headers || {}
init.headers['X-Wirecore-PublicKey'] = pubKey
init.headers['X-Wirecore-Signature'] = signature
init.headers['X-Wirecore-Challenge'] = ch.challengeId
cachedChallenge = null
var res = await originalFetch(input, init)
if (res.status === 401) {
cachedChallenge = null
ch = await getChallenge()
signature = await signChallenge(ch.challenge)
init.headers['X-Wirecore-PublicKey'] = pubKey
init.headers['X-Wirecore-Signature'] = signature
init.headers['X-Wirecore-Challenge'] = ch.challengeId
cachedChallenge = null
res = await originalFetch(input, init)
}
return res
} catch (err) {
console.error('[Identity] Auth interception failed:', err)
return originalFetch(input, init)
}
}
window.fetch = authenticatedFetch
// ==================== PUBLIC API ====================
return {
getKeypair: getKeypair,
getPublicKey: getPublicKey,
getUsername: getUsername,
setUsername: setUsername,
generate: generate,
getChallenge: getChallenge,
signChallenge: signChallenge,
hasKeypair: function () { return !!getKeypair() },
loadNoble: loadNoble,
clear: clear,
importKeypair: importKeypair,
exportKeypair: exportKeypair
}
})()