OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking commits / bcb05ae

Fix identity.js: vendored noble libs, zero CDN, rename to HyperBookingIdentity
- Replaced esm.sh CDN imports with local /web/vendor/ paths - Renamed window.HnIdentity → window.HyperBookingIdentity - Renamed localStorage keys hn-* → hb-* - Challenge endpoint uses GET (matches server route) - All crypto libs vendored locally (noble-ed25519, noble-sha512) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bcb05ae77a99c2cbf8f4dc905636fa6e9951327e
by plankatron <fleeky@gmail.com> · 2026-03-22T18:09:42.000Z

Changed files (1) +18 -76

web/js/identity.js +18 -76
……@@ -1,27 +1,28 @@
11/**
2 * Hypernote Identity — Browser ed25519 auth
2 * hyper-booking Identity — Browser ed25519 auth
33 *
44 * Generates ed25519 keypair CLIENT-SIDE using @noble/ed25519.
55 * The secret key never leaves the browser.
66 * Uses challenge-response signing for every authenticated request.
77 * Intercepts fetch() to inject auth headers automatically.
8 *
9 * Adapted from hypernote/pear-git identity.js.
10 * Uses vendored noble libs (zero CDN).
811 */
912
10window.HnIdentity = (function () {
11 var KEYPAIR_KEY = 'hn-keypair'
12 var PUBKEY_KEY = 'hn-pubkey'
13 var USERNAME_KEY = 'hn-username'
13window.HyperBookingIdentity = (function () {
14 var KEYPAIR_KEY = 'hb-keypair'
15 var PUBKEY_KEY = 'hb-pubkey'
16 var USERNAME_KEY = 'hb-username'
1417
15 // Challenge cache — reuse until 1 min before expiry
1618 var cachedChallenge = null
1719
18 // @noble/ed25519 loaded via CDN
1920 var ed = null
2021 var sha512 = null
2122 var nobleLoading = null
2223
2324 /**
24 * Load @noble/ed25519 from CDN
25 * Load @noble/ed25519 from vendored files
2526 */
2627 function loadNoble () {
2728 if (ed) return Promise.resolve()
……@@ -29,9 +30,9 @@ window.HnIdentity = (function () {
2930
3031 nobleLoading = (async function () {
3132 try {
32 var module = await import('https://esm.sh/@noble/ed25519@2.0.0')
33 var module = await import('/web/vendor/noble-ed25519.js')
3334 ed = module
34 var hashModule = await import('https://esm.sh/@noble/hashes@1.3.3/sha512')
35 var hashModule = await import('/web/vendor/noble-sha512.js')
3536 sha512 = hashModule.sha512
3637 ed.etc.sha512Sync = function () {
3738 return sha512.apply(null, arguments)
……@@ -46,30 +47,18 @@ window.HnIdentity = (function () {
4647 return nobleLoading
4748 }
4849
49 /**
50 * Get stored keypair or null
51 */
5250 function getKeypair () {
5351 return localStorage.getItem(KEYPAIR_KEY) || null
5452 }
5553
56 /**
57 * Get stored public key
58 */
5954 function getPublicKey () {
6055 return localStorage.getItem(PUBKEY_KEY) || null
6156 }
6257
63 /**
64 * Get stored username
65 */
6658 function getUsername () {
6759 return localStorage.getItem(USERNAME_KEY) || null
6860 }
6961
70 /**
71 * Set stored username
72 */
7362 function setUsername (u) {
7463 if (u) {
7564 localStorage.setItem(USERNAME_KEY, u)
……@@ -78,28 +67,21 @@ window.HnIdentity = (function () {
7867 }
7968 }
8069
81 /**
82 * Generate new ed25519 keypair CLIENT-SIDE
83 * Secret key never leaves the browser.
84 */
8570 async function generate () {
8671 await loadNoble()
8772
88 // Generate 32-byte random seed using browser crypto
8973 var seed = new Uint8Array(32)
9074 crypto.getRandomValues(seed)
9175
92 // Derive ed25519 public key from seed
9376 var publicKeyBytes = await ed.getPublicKeyAsync(seed)
9477
95 // Build 64-byte secret key (seed + public key, matching hypercore-crypto format)
9678 var secretKeyBytes = new Uint8Array(64)
9779 secretKeyBytes.set(seed, 0)
9880 secretKeyBytes.set(publicKeyBytes, 32)
9981
100 var publicKeyB64 = bytesToBase64(publicKeyBytes) // 44 chars
101 var secretKeyB64 = bytesToBase64(secretKeyBytes) // 88 chars
102 var keypair = publicKeyB64 + secretKeyB64 // 132 chars
82 var publicKeyB64 = bytesToBase64(publicKeyBytes)
83 var secretKeyB64 = bytesToBase64(secretKeyBytes)
84 var keypair = publicKeyB64 + secretKeyB64
10385
10486 localStorage.setItem(KEYPAIR_KEY, keypair)
10587 localStorage.setItem(PUBKEY_KEY, publicKeyB64)
……@@ -107,28 +89,18 @@ window.HnIdentity = (function () {
10789 return { publicKey: publicKeyB64, keypair: keypair }
10890 }
10991
110 /**
111 * Get a fresh or cached challenge
112 */
11392 async function getChallenge () {
11493 if (cachedChallenge) {
11594 var remaining = cachedChallenge.expiresAt - Date.now()
11695 if (remaining > 60000) return cachedChallenge
11796 }
11897
119 var base = (window.HnApi && window.HnApi.basePath) || ''
120 var res = await originalFetch(base + '/api/auth/challenge', {
121 method: 'POST',
122 headers: { 'Content-Type': 'application/json' }
123 })
98 var res = await originalFetch('/api/auth/challenge')
12499 if (!res.ok) throw new Error('Failed to get challenge')
125100 cachedChallenge = await res.json()
126101 return cachedChallenge
127102 }
128103
129 /**
130 * Sign a challenge string with the stored keypair
131 */
132104 async function signChallenge (challenge) {
133105 await loadNoble()
134106 var keypair = getKeypair()
……@@ -137,7 +109,6 @@ window.HnIdentity = (function () {
137109 var secretKeyB64 = keypair.substring(44, 132)
138110 var secretKey = base64ToBytes(secretKeyB64)
139111
140 // noble expects the 32-byte seed (first 32 bytes of 64-byte secret key)
141112 var seed = secretKey.slice(0, 32)
142113 var challengeBytes = new TextEncoder().encode(challenge)
143114 var signature = ed.sign(challengeBytes, seed)
……@@ -158,20 +129,13 @@ window.HnIdentity = (function () {
158129 return btoa(bin)
159130 }
160131
161 /**
162 * Clear identity (logout)
163 */
164132 function clear () {
165133 localStorage.removeItem(KEYPAIR_KEY)
166134 localStorage.removeItem(PUBKEY_KEY)
167135 localStorage.removeItem(USERNAME_KEY)
168 localStorage.removeItem('hn-token')
169136 cachedChallenge = null
170137 }
171138
172 /**
173 * Import keypair from 132-char string
174 */
175139 function importKeypair (keypairStr) {
176140 if (!keypairStr || keypairStr.length !== 132) {
177141 throw new Error('Invalid keypair (must be 132 chars)')
……@@ -183,9 +147,6 @@ window.HnIdentity = (function () {
183147 return pubKey
184148 }
185149
186 /**
187 * Export keypair as JSON object (for saving to file or copying)
188 */
189150 function exportKeypair () {
190151 var keypair = getKeypair()
191152 var pubKey = getPublicKey()
……@@ -201,17 +162,9 @@ window.HnIdentity = (function () {
201162
202163 // ==================== FETCH INTERCEPTION ====================
203164
204 /**
205 * Skip auth when user has no identity (anonymous browsing — reads are open).
206 * When user HAS an identity, always send auth headers (even for GETs like profile).
207 */
208 function shouldSkipAuth (url, method) {
165 function shouldSkipAuth (url) {
209166 if (url.indexOf('/api/') === -1) return true
210 // Challenge endpoint must be unauthenticated
211167 if (url.indexOf('/api/auth/challenge') !== -1) return true
212 // Username availability check is public
213 if (url.indexOf('/available') !== -1) return true
214 // If no identity, skip auth (anonymous browsing — reads are open)
215168 if (!getPublicKey()) return true
216169 return false
217170 }
……@@ -220,9 +173,8 @@ window.HnIdentity = (function () {
220173
221174 async function authenticatedFetch (input, init) {
222175 var url = typeof input === 'string' ? input : input.url || ''
223 var method = (init && init.method) ? init.method.toUpperCase() : 'GET'
224176
225 if (shouldSkipAuth(url, method)) {
177 if (shouldSkipAuth(url)) {
226178 return originalFetch(input, init)
227179 }
228180
……@@ -231,15 +183,6 @@ window.HnIdentity = (function () {
231183 return originalFetch(input, init)
232184 }
233185
234 // Legacy bearer token support
235 var legacyToken = localStorage.getItem('hn-token')
236 if (legacyToken) {
237 init = init || {}
238 init.headers = init.headers || {}
239 init.headers['Authorization'] = 'Bearer ' + legacyToken
240 return originalFetch(input, init)
241 }
242
243186 try {
244187 var ch = await getChallenge()
245188 var signature = await signChallenge(ch.challenge)
……@@ -254,7 +197,6 @@ window.HnIdentity = (function () {
254197
255198 var res = await originalFetch(input, init)
256199
257 // On 401, retry once with fresh challenge
258200 if (res.status === 401) {
259201 cachedChallenge = null
260202 ch = await getChallenge()
……@@ -287,7 +229,7 @@ window.HnIdentity = (function () {
287229 generate: generate,
288230 getChallenge: getChallenge,
289231 signChallenge: signChallenge,
290 hasIdentity: function () { return !!getKeypair() },
232 hasKeypair: function () { return !!getKeypair() },
291233 loadNoble: loadNoble,
292234 clear: clear,
293235 importKeypair: importKeypair,