OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / lib / identity-registry.js

178 lines · javascript ref:
import fs from 'bare-fs'
import path from 'bare-path'

const REGISTRY_FILE = 'identities.json'
const REGISTRY_VERSION = 1

export class IdentityRegistry {
  constructor (baseDir) {
    this.baseDir = baseDir
    this.filePath = path.join(baseDir, REGISTRY_FILE)
    this.data = { version: REGISTRY_VERSION, activeId: null, identities: [] }
  }

  async load () {
    try {
      const raw = await fs.promises.readFile(this.filePath, 'utf-8')
      this.data = JSON.parse(raw)
    } catch {
      // No registry — run migration
      await this._migrate()
    }
    return this.data
  }

  async save () {
    const tmp = this.filePath + '.tmp'
    await fs.promises.writeFile(tmp, JSON.stringify(this.data, null, 2), 'utf-8')
    await fs.promises.rename(tmp, this.filePath)
  }

  list () {
    return this.data.identities
  }

  get (id) {
    return this.data.identities.find(e => e.id === id) || null
  }

  getActive () {
    return this.get(this.data.activeId)
  }

  getActiveId () {
    return this.data.activeId
  }

  async create ({ name }) {
    const nextNum = this.data.identities.length
    const id = 'identity-' + nextNum
    const dir = path.join(this.baseDir, id)

    await fs.promises.mkdir(dir, { recursive: true })

    const entry = {
      id,
      name: name || id,
      username: '',
      dir,
      created: new Date().toISOString()
    }

    this.data.identities.push(entry)
    await this.save()
    return entry
  }

  async setActive (id) {
    if (!this.get(id)) throw new Error('Identity not found: ' + id)
    this.data.activeId = id
    await this.save()
  }

  async updateMeta (id, fields) {
    const entry = this.get(id)
    if (!entry) throw new Error('Identity not found: ' + id)
    if (fields.name !== undefined) entry.name = fields.name
    if (fields.username !== undefined) entry.username = fields.username
    // The identity's home room (router home.get). Lives with the identity, so an
    // identity switch or restore switches homes with it.
    if (fields.homeRoomId !== undefined) entry.homeRoomId = fields.homeRoomId
    await this.save()
    return entry
  }

  async remove (id, deleteFiles) {
    if (this.data.activeId === id) throw new Error('Cannot delete active identity')
    const entry = this.get(id)
    if (!entry) throw new Error('Identity not found: ' + id)

    this.data.identities = this.data.identities.filter(e => e.id !== id)
    await this.save()

    if (deleteFiles && entry.dir) {
      await fs.promises.rm(entry.dir, { recursive: true, force: true })
    }

    return { ok: true }
  }

  getStoragePath (id) {
    const entry = this.get(id)
    if (!entry) throw new Error('Identity not found: ' + id)
    return entry.dir
  }

  async _migrate () {
    const defaultDir = path.join(this.baseDir, 'default')
    const targetId = 'identity-0'
    const targetDir = path.join(this.baseDir, targetId)

    let hasDefault = false
    try {
      await fs.promises.stat(defaultDir)
      hasDefault = true
    } catch {}

    if (hasDefault) {
      // Rename default/ -> identity-0/
      try {
        await fs.promises.rename(defaultDir, targetDir)
      } catch (err) {
        // If target already exists, default was already migrated
        if (err.code !== 'ENOTEMPTY' && err.code !== 'EEXIST') throw err
      }

      this.data = {
        version: REGISTRY_VERSION,
        activeId: targetId,
        identities: [{
          id: targetId,
          name: 'Default',
          username: '',
          dir: targetDir,
          created: new Date().toISOString()
        }]
      }
    } else {
      // Check if identity-0 already exists (previous partial migration)
      let hasTarget = false
      try {
        await fs.promises.stat(targetDir)
        hasTarget = true
      } catch {}

      if (hasTarget) {
        this.data = {
          version: REGISTRY_VERSION,
          activeId: targetId,
          identities: [{
            id: targetId,
            name: 'Default',
            username: '',
            dir: targetDir,
            created: new Date().toISOString()
          }]
        }
      } else {
        // Fresh install — create identity-0
        await fs.promises.mkdir(targetDir, { recursive: true })
        this.data = {
          version: REGISTRY_VERSION,
          activeId: targetId,
          identities: [{
            id: targetId,
            name: 'Default',
            username: '',
            dir: targetDir,
            created: new Date().toISOString()
          }]
        }
      }
    }

    await this.save()
    console.log('[identity-registry] Migration complete — active:', this.data.activeId)
  }
}