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