/**
* Hypernote API Client (browser)
*
* When loaded via hyper://, API calls go to the configured local server.
* The API base is stored in localStorage ('hn-api-base') and can be set
* from the UI (defaults to http://localhost:4040).
*
* Auth headers are injected automatically by identity.js fetch interception.
* Bearer tokens still work for backwards compatibility (CLI/API users).
*/
window.HnApi = (function () {
// Detect hyper:// mode (Agregore browser accessing a Hyperdrive)
var isHyperMode = window.location.protocol === 'hyper:'
// Legacy bearer token support (for CLI/API)
function getToken () {
return localStorage.getItem('hn-token') || ''
}
function setToken (token) {
localStorage.setItem('hn-token', token)
}
// In hyper mode, API calls go to the local HTTP server
// In normal mode, resolve from current page URL (supports reverse proxy prefixes)
var basePath
if (isHyperMode) {
basePath = localStorage.getItem('hn-api-base') || 'http://localhost:4040'
} else {
basePath = window.location.pathname.replace(/\/+$/, '')
}
function getApiBase () { return basePath }
function setApiBase (url) {
basePath = url.replace(/\/+$/, '')
localStorage.setItem('hn-api-base', basePath)
}
// ==================== HTTP MODE ====================
async function request (method, path, body) {
var opts = { method, headers: { 'Content-Type': 'application/json' } }
if (body) opts.body = JSON.stringify(body)
// Prepend base path for absolute API paths
var url = path.startsWith('/') ? basePath + path : path
// fetch() is intercepted by identity.js to inject auth headers
var res = await fetch(url, opts)
var data = await res.json()
if (!res.ok) {
var err = new Error(data.error || res.statusText)
err.status = res.status
err.data = data
throw err
}
return data
}
// ==================== PUBLIC API ====================
return {
basePath,
isHyperMode: isHyperMode,
getToken,
setToken,
getApiBase,
setApiBase,
get: (path) => request('GET', path),
post: (path, body) => request('POST', path, body),
patch: (path, body) => request('PATCH', path, body),
del: (path) => request('DELETE', path),
// Feed
recentNotes: (limit) => request('GET', '/api/recent' + (limit ? '?limit=' + limit : '')),
// Notebooks
listNotebooks: () => request('GET', '/api/notebooks'),
listMyNotebooks: () => request('GET', '/api/notebooks?mine=true'),
getNotebook: (slug) => request('GET', '/api/notebooks/' + encodeURIComponent(slug)),
createNotebook: (body) => request('POST', '/api/notebooks', body),
updateNotebook: (slug, body) => request('PATCH', '/api/notebooks/' + encodeURIComponent(slug), body),
deleteNotebook: (slug) => request('DELETE', '/api/notebooks/' + encodeURIComponent(slug)),
// Notes (id can be slug or hex ID)
listNotes: (nb) => request('GET', '/api/notebooks/' + encodeURIComponent(nb) + '/notes'),
getNote: (nb, id) => request('GET', '/api/notebooks/' + encodeURIComponent(nb) + '/notes/' + encodeURIComponent(id)),
createNote: (nb, body) => request('POST', '/api/notebooks/' + encodeURIComponent(nb) + '/notes', body),
updateNote: (nb, id, body) => request('PATCH', '/api/notebooks/' + encodeURIComponent(nb) + '/notes/' + encodeURIComponent(id), body),
deleteNote: (nb, id) => request('DELETE', '/api/notebooks/' + encodeURIComponent(nb) + '/notes/' + encodeURIComponent(id)),
// README
getReadme: (nb) => request('GET', '/api/notebooks/' + encodeURIComponent(nb) + '/readme'),
regenerateReadme: (nb) => request('POST', '/api/notebooks/' + encodeURIComponent(nb) + '/readme/regenerate'),
// Search
search: (q, nb) => {
var path = '/api/search?q=' + encodeURIComponent(q)
if (nb) path += '¬ebook=' + encodeURIComponent(nb)
return request('GET', path)
},
// Links
getLinks: (nb, id) => request('GET', '/api/notebooks/' + encodeURIComponent(nb) + '/notes/' + id + '/links'),
getBacklinks: (nb, id) => request('GET', '/api/notebooks/' + encodeURIComponent(nb) + '/notes/' + id + '/backlinks'),
getGraph: (nb) => request('GET', nb ? '/api/graph?notebook=' + encodeURIComponent(nb) : '/api/graph'),
// Advanced
mergeNotes: (nb, id1, id2, title) => request('POST', '/api/notebooks/' + encodeURIComponent(nb) + '/merge', { noteId1: id1, noteId2: id2, title }),
splitNote: (nb, id, delimiter) => request('POST', '/api/notebooks/' + encodeURIComponent(nb) + '/notes/' + id + '/split', { delimiter }),
unlockNotebook: (nb, password) => request('POST', '/api/notebooks/' + encodeURIComponent(nb) + '/unlock', { password }),
// Users
listUsers: () => request('GET', '/api/users'),
getUserNotebooks: (username) => request('GET', '/api/notebooks?owner=' + encodeURIComponent(username)),
// Identity & Profile (Phase 7)
whoami: () => request('GET', '/api/whoami'),
getProfile: () => request('GET', '/api/user/profile'),
updateProfile: (updates) => request('PATCH', '/api/user/profile', updates),
setUsername: (username) => request('POST', '/api/user/username', { username }),
checkUsername: (username) => request('GET', '/api/user/username/' + encodeURIComponent(username) + '/available'),
// Multi-Writer
multiwriterStatus: () => request('GET', '/api/multiwriter/status'),
multiwriterLink: () => request('POST', '/api/multiwriter/link', {}),
multiwriterJoin: (invite) => request('POST', '/api/multiwriter/join', { invite }),
multiwriterSync: () => request('POST', '/api/multiwriter/sync', {}),
multiwriterAddWriter: (writerKey) => request('POST', '/api/multiwriter/add-writer', { writerKey }),
multiwriterMirror: (enabled) => request('POST', '/api/multiwriter/mirror', { enabled }),
shareNotebook: (slug, shared) => request('POST', '/api/notebooks/' + encodeURIComponent(slug) + '/share', { shared }),
// Admin
adminUsers: () => request('GET', '/api/admin/users'),
adminStats: () => request('GET', '/api/admin/stats'),
adminPromote: (userId, role) => request('POST', '/api/admin/promote', { userId, role }),
adminDemote: (userId, role) => request('POST', '/api/admin/demote', { userId, role }),
adminDeleteUser: (userId) => request('DELETE', '/api/admin/users/' + userId),
adminTransferNotebook: (slug, ownerId) => request('POST', '/api/admin/notebooks/' + encodeURIComponent(slug) + '/transfer', { ownerId })
}
})()