OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / web / js / api.js

145 lines · javascript ref:
/**
 * 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 += '&notebook=' + 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 })
  }
})()