OverviewFilesCommitsIssuesDocsReleasesPeers

keet-deobfuscated / @holepunchto / keet-registry / index.js

346 lines · javascript ref:
const e = require("ready-resource"),
  t = require("hyperdb"),
  r = require("hyperblobs"),
  i = require("hypercore-id-encoding"),
  s = require("keet-identity-key"),
  a = require("hypercore-crypto"),
  n = require("compact-encoding"),
  o = require("b4a"),
  c = require("./spec/hyperschema"),
  h = require("./spec/hyperdb"),
  l = require("./lib/errors.js"),
  d = require("./lib/map-stream.js"),
  y = require("./lib/watcher.js"),
  [u] = a.namespace("keet-registry", 1),
  g = c.getEncoding("@keet/member-profile"),
  m = 524288
class v extends e {
  static IDENTITY_ENCODING = g
  static MAX_RECORDS_PER_KEY = 5
  static MAX_RESERVATIONS_PER_KEY = 5
  constructor(e, i, s, { extension: a = !0 } = {}) {
    ;(super(),
      (this.db = t.bee(e, h, { extension: a, autoUpdate: !0 })),
      (this.reserved = t.bee(i, h, { extension: a, autoUpdate: !0 })),
      (this.blobs = new r(s)),
      (this._inflateRecordBound = this._inflateRecord.bind(this)),
      (this._recordWatcher = null),
      (this._reservationWatcher = null))
  }
  get key() {
    return this.db.core.key
  }
  get discoveryKey() {
    return this.db.core.discoveryKey
  }
  get keys() {
    return { db: w(this.db.core), reserved: w(this.reserved.core), blobs: w(this.blobs.core) }
  }
  async _open() {
    await this.db.ready()
  }
  async _close() {
    ;(this.unwatch(), await this.db.close(), await this.reserved.close(), await this.blobs.close())
  }
  async put({ profile: e, attestation: t } = {}) {
    const r = i.decode(t.memberKey)
    this.opened || (await this.ready())
    const s = { version: 3, count: 1, profile: e, attestation: t }
    ;(await v.verifyPut(this, s), (s.profile = await this._extractProfile(e)))
    const a = await this.getByKey(r),
      n = this.db.transaction(),
      o = this.reserved.transaction()
    a &&
      s.profile.username !== a.profile.username &&
      (await n.delete("@keet-registry-v3/identity", a), (s.count = a.count + 1))
    const c = { username: s.profile.username, memberKey: t.memberKey },
      h = [
        o.insert("@keet-registry-v3/reserved", c),
        n.insert("@keet-registry-v3/attestation", t),
        n.insert("@keet-registry-v3/identity", s),
      ]
    ;(await Promise.all(h), await Promise.all([n.flush(), o.flush()]))
  }
  async isAvailable(e) {
    return !(await this.getReserved(e)) && !(await this.get(e))
  }
  async reserve(e) {
    ;(this.opened || (await this.ready()), await v.verifyReserve(this, e))
    const { username: t, attestation: r } = e,
      i = { username: t, memberKey: r.memberKey },
      s = this.reserved.transaction()
    ;(await s.insert("@keet-registry-v3/reserved", i), await s.flush())
  }
  async getReserved(e) {
    return (
      this.opened || (await this.ready()),
      this.reserved.get("@keet-registry-v3/reserved", { username: e })
    )
  }
  async block(e, { reason: t = "" } = {}) {
    this.opened || (await this.ready())
    const r = await this.getByKey(e),
      i = this.db.transaction()
    ;(r && (await i.delete("@keet-registry-v3/identity", r)),
      await i.insert("@keet-registry-v3/blocked", { memberKey: e, reason: t }),
      await i.flush())
  }
  async getBlocked(e) {
    return (
      this.opened || (await this.ready()),
      await this.db.get("@keet-registry-v3/blocked", { memberKey: e })
    )
  }
  async isBlocked(e) {
    return (this.opened || (await this.ready()), null !== (await this.getBlocked(e)))
  }
  async get(e) {
    this.opened || (await this.ready())
    const t = await this.db.get("@keet-registry-v3/identity", { profile: { username: e } })
    return t ? this._inflateRecord(t) : null
  }
  async _extractProfile(e) {
    return { ...e, avatar: await k(this.blobs, e.avatar) }
  }
  async _inflateProfile(e) {
    return { ...e, avatar: await K(this.blobs, e.avatar) }
  }
  async _inflateRecord(e) {
    return {
      ...e,
      profile: await this._inflateProfile(e.profile),
      attestation: await this._getAttestation(e.attestation, this.db),
    }
  }
  _getAttestation({ memberKey: e }, t) {
    return t.get("@keet-registry-v3/attestation", { memberKey: e })
  }
  async _getProfileByKey(e, t) {
    const r = { attestation: { memberKey: e } }
    return t.get("@keet-registry-v3/identity-by-key", r)
  }
  async updateManifest(e) {
    const t = await this.db.get("@keet-registry-v3/manifest")
    if (b(t, e)) return
    if (t && e.version < t.version) throw l.INVALID_MANIFEST()
    t || (await this._migrateFromV2())
    const r = this.db.transaction()
    ;(await r.insert("@keet-registry-v3/manifest", e), await r.flush())
  }
  async _migrateFromV2() {
    const e = this.reserved.transaction(),
      t = []
    for await (const { name: r, publicKey: i } of this._v2ListAll())
      t.push(e.insert("@keet-registry-v3/reserved", { username: r, memberKey: i }))
    return (await Promise.all(t), e.flush())
  }
  async v2Get(e) {
    return (
      this.opened || (await this.ready()),
      this.db.get("@keet-registry/identity", { name: e })
    )
  }
  async has(e) {
    return null !== (await this.get(e))
  }
  async hasKey(e) {
    return null !== (await this.getByKey(e))
  }
  async getByKey(e) {
    const t = i.decode(e)
    this.opened || (await this.ready())
    const r = { attestation: { memberKey: t } },
      s = this.db.snapshot(),
      a = [this._getProfileByKey(t, s), this._getAttestation(r.attestation, s)],
      [n, o] = await Promise.all(a)
    return (await s.close(), n ? { ...n, attestation: o } : null)
  }
  async getReservationsByKey(e) {
    const t = i.decode(e)
    this.opened || (await this.ready())
    const r = { memberKey: t }
    return this.reserved.find("@keet-registry-v3/reserved-by-key", { gte: r, lte: r }).toArray()
  }
  async getNrRecordsByKey(e) {
    const t = await this._getProfileByKey(e, this.db)
    return t ? t.count : 0
  }
  async getNrReservationsByKey(e) {
    return (await this.getReservationsByKey(e)).length
  }
  list(e = {}, t = {}) {
    const r = this.db.find("@keet-registry-v3/identity", e, t)
    return new d(r, this._inflateRecordBound)
  }
  _v2ListAll() {
    return this.db.find("@keet-registry/identity")
  }
  search(e, t = {}) {
    if ((t.limit || (t.limit = 10), 0 === e.length)) return this.list({}, t)
    const r = e.slice(0, e.length - 1) + String.fromCharCode(e.charCodeAt(e.length - 1) + 1),
      i = { gte: { profile: { username: e } }, lt: { profile: { username: r } } }
    return this.list(i, t)
  }
  unwatch() {
    ;(this._recordWatcher && this._recordWatcher.destroy(),
      this._reservationWatcher && this._reservationWatcher.destroy(),
      (this._recordWatcher = null),
      (this._reservationWatcher = null))
  }
  _getRecordWatcher() {
    return (
      this._recordWatcher ||
        (this._recordWatcher = new y(this.db, "@keet-registry-v3/identity-by-key")),
      this._recordWatcher
    )
  }
  watchRecord(e, t) {
    return this._getRecordWatcher().add(e, t)
  }
  _getReservationWatcher() {
    return (
      this._reservationWatcher ||
        (this._reservationWatcher = new y(this.reserved, "@keet-registry-v3/reserved")),
      this._reservationWatcher
    )
  }
  watchReservation(e) {
    return this._getReservationWatcher().add({ username: e })
  }
  registerMetrics(e) {
    const t = this
    ;(new e.Gauge({
      name: "keet_registry_db_version",
      help: "The current version of the records db",
      collect() {
        this.set(t.db.core.length)
      },
    }),
      new e.Gauge({
        name: "keet_registry_reserved_version",
        help: "The current version of the reserved db",
        collect() {
          this.set(t.reserved.core.length)
        },
      }),
      new e.Gauge({
        name: "keet_registry_blobs_version",
        help: "The current version of the blobs core (with avatars)",
        collect() {
          this.set(t.blobs.core.length)
        },
      }),
      new e.Gauge({
        name: "keet_registry_db_signed_length",
        help: "The current signed length of the records db",
        collect() {
          this.set(t.db.core.signedLength)
        },
      }),
      new e.Gauge({
        name: "keet_registry_reserved_signed_length",
        help: "The current signed length of the reserved db",
        collect() {
          this.set(t.reserved.core.signedLength)
        },
      }),
      new e.Gauge({
        name: "keet_registry_blobs_signed_length",
        help: "The current signed length of the blobs core (with avatars)",
        collect() {
          this.set(t.blobs.core.signedLength)
        },
      }))
  }
  static async verifyReserve(e, t) {
    if (!v.verifyReservation(t)) throw l.INVALID_ATTESTATION()
    const { username: r, attestation: i } = t,
      { memberKey: s } = i,
      [a, n, o, c] = await Promise.all([
        e.isBlocked(s),
        e.get(r),
        e.getNrReservationsByKey(s),
        e.getReserved(r),
      ])
    if (a) throw l.BLOCKED()
    if (n || c) throw l.NAME_UNAVAILABLE()
    if (o >= v.MAX_RESERVATIONS_PER_KEY) throw l.EXCEEDS_RESERVATION_LIMIT()
  }
  static async verifyPut(e, t) {
    if (!v.verifyAttestation(t)) throw l.INVALID_ATTESTATION()
    const { timestamp: r, username: i, avatar: s } = t.profile
    if (r < 0) throw new l("Negative timestamp value")
    if (!i) throw l.INVALID_NAME()
    if (s && !_(s)) throw l.INVALID_AVATAR()
    const { memberKey: a } = t.attestation,
      [n, c, h, d, y] = await Promise.all([
        e.isBlocked(a),
        e.getByKey(a, { attestation: !1 }),
        e.get(i),
        e.getNrRecordsByKey(a),
        e.getReserved(i),
      ])
    if (n) throw l.BLOCKED()
    const u = (h && h.attestation) || y
    if (u && !o.equals(u.memberKey, a)) throw l.NAME_UNAVAILABLE()
    if (d >= v.MAX_RECORDS_PER_KEY) throw l.EXCEEDS_RECORD_LIMIT()
    if (c && r <= c.profile.timestamp) throw l.OUTDATED_CLOCK()
  }
  static encodeReservation({ username: e }) {
    const t = o.alloc(32 + e.length),
      r = { start: 0, end: t.length, buffer: t }
    return (n.fixed32.encode(r, u), n.string.encode(r, e), t)
  }
  static verifyReservation({ username: e, attestation: t }) {
    return f(t, this.encodeReservation({ username: e }))
  }
  static verifyAttestation({ profile: e, attestation: t }) {
    return f(t, n.encode(g, e))
  }
}
function b(e, t) {
  return !!e && e.version === t.version
}
function f(e, t, r) {
  return !!s.verify(e.proof, t, { expectedIdentity: e.memberKey })
}
function w(e) {
  return { key: e.key, discoveryKey: e.discoveryKey }
}
function _(e) {
  return !((e.small && !p(e.small)) || (e.medium && !p(e.medium)) || (e.large && !p(e.large)))
}
function p(e) {
  return !e || (!!e.pointer.inlined && e.pointer.inlined.byteLength <= m)
}
async function k(e, t) {
  if (!t) return t
  const { small: r, medium: i, large: s } = t
  return {
    small: r ? await R(e, r) : null,
    medium: i ? await R(e, i) : null,
    large: s ? await R(e, s) : null,
  }
}
async function K(e, t) {
  if (!t) return t
  const { small: r, medium: i, large: s } = t
  return {
    small: r ? await A(e, r) : null,
    medium: i ? await A(e, i) : null,
    large: s ? await A(e, s) : null,
  }
}
async function R(e, t) {
  if (!p(t)) throw new Error("Cannot inline a blob that is larger than 512 KB")
  const r = await e.put(t.pointer.inlined)
  return { ...t, pointer: { blob: r } }
}
async function A(e, t) {
  if (!t) return t
  if (!t.pointer.blob) return t
  const r = await e.get(t.pointer.blob)
  return { ...t, pointer: { inlined: r } }
}
module.exports = v