OverviewFilesCommitsIssuesDocsReleasesPeers

keet-deobfuscated / @holepunchto / keet-registry-client / bin.js

445 lines · javascript ref:
#!/usr/bin/env node
const { once: e } = require("events"),
  o = require("path"),
  t = require("os"),
  r = require("fs").promises,
  a = require("hypercore-crypto"),
  i = require("hyperswarm"),
  s = require("hypercore-id-encoding"),
  { command: n, flag: c, arg: l, description: d } = require("paparam"),
  y = require("corestore"),
  u = require("graceful-goodbye"),
  g = require("z32"),
  h = require("keet-identity-key"),
  m = require("@holepunchto/keet-registry"),
  p = require("protomux-rpc-client"),
  k = require("compact-encoding"),
  f = require("."),
  b = m.IDENTITY_ENCODING,
  w = "keet-registry-client",
  K = o.resolve(o.join(t.homedir(), ".holepunch-admin", "swarm-keypair.json")),
  v = n(
    "identity",
    c(
      "--key-pair-location|-k [keyPairLocation]",
      `Path of the admin keypair file, defaults to ${K}`,
    ),
    async function ({ flags: e }) {
      const t = o.resolve(e.keyPairLocation || K)
      console.log(`Using keypair file: ${t}`)
      const r = await z(t)
      console.log(`Swarm public key: ${s.normalize(r.publicKey)}`)
    },
  ),
  P = n(
    "get-ip",
    d(
      "De-obfuscate an IP (admin only: ensure your keypair is added as an admin-key to the registry you are contacting)",
    ),
    l(
      "<registryPublicKey>",
      "Public RPC key of the keet-registry indexer to which to send the request",
    ),
    l("<obfuscatedIp>", "Obfuscated IP to get the actual IP for"),
    c(
      "--key-pair-location|-k [keyPairLocation]",
      `Path of the admin keypair file, defaults to ${K}`,
    ),
    async function ({ args: e, flags: t }) {
      const r = o.resolve(t.keyPairLocation || K),
        a = s.decode(e.registryPublicKey),
        n = await z(r)
      console.log(`Using admin swarm public key ${s.normalize(n.publicKey)} (loaded from ${r})`)
      const c = g.decode(e.obfuscatedIp),
        l = new i(),
        d = new p(l.dht, { keyPair: n }),
        y = new f([a], d)
      let u = 0
      try {
        const e = await y.getIp(c)
        console.log(`${g.encode(c)} -> ${e}`)
      } catch (e) {
        ;((u = 1),
          "NO_ACCESS" === e.code
            ? console.error("You are not an admin for the registry")
            : console.error(e))
      }
      ;(y.destroy(), await l.destroy(), await d.close(), process.exit(u))
    },
  ),
  E = n(
    "block",
    d(
      "Block a member by public key (admin only: ensure your keypair is added as an admin-key to the registry you are contacting)",
    ),
    l(
      "<registryPublicKey>",
      "Public RPC key of the keet-registry indexer to which to send the request",
    ),
    l("<memberKey>", "Public key of the member you want to block (hex or z32)"),
    c(
      "--key-pair-location|-k [keyPairLocation]",
      `Path of the admin keypair file, defaults to ${K}`,
    ),
    async function ({ args: e, flags: t }) {
      const r = o.resolve(t.keyPairLocation || K),
        a = s.decode(e.registryPublicKey),
        n = await z(r),
        c = s.decode(e.memberKey)
      console.log(`Using admin swarm public key ${s.normalize(n.publicKey)} (loaded from ${r})`)
      const l = new i(),
        d = new p(l.dht, { keyPair: n }),
        y = new f([a], d)
      let u = 0
      try {
        ;(await y.block({ memberKey: c }), console.log(`blocked member ${s.normalize(c)}`))
      } catch (e) {
        ;((u = 1),
          "NO_ACCESS" === e.code
            ? console.error("You are not an admin for the registry")
            : console.error(e))
      }
      ;(y.destroy(), await l.destroy(), await d.close(), process.exit(u))
    },
  ),
  $ = n(
    "register",
    l(
      "<registryPublicKey>",
      "Public RPC key of the keet-registry indexer to which to send the request",
    ),
    l("<name>", "Name to register"),
    l("<publicKey>", "Public key the name should point to"),
    l("<privateKey>", "Private key corresponding to the public key (in z32 encoding)"),
    async function ({ args: e, flags: o }) {
      const t = s.decode(e.registryPublicKey),
        r = s.decode(e.publicKey),
        a = g.decode(e.privateKey)
      if (64 !== a.byteLength) throw new Error("Invalid secret key")
      const n = { publicKey: r, secretKey: a },
        { name: c } = e,
        l = new i(),
        d = new p(l.dht),
        y = new f([t], d)
      let u = 0
      console.log("Registering record with the keet-registry service...")
      try {
        const e = L(c, n)
        ;(await y.putRecord(e),
          console.log(`Successfully requested to register ${c} -> ${s.normalize(r)}`))
      } catch (e) {
        ;((u = 1),
          "KEY_EXISTS" === e.code
            ? console.error(
                "This public key is already registered to another name (use the update command instead)",
              )
            : "NAME_EXISTS" === e.code
              ? console.error("This name is already taken")
              : "NAME_EXISTED" === e.code
                ? console.error("This name has been used before, so cannot be registered anymore")
                : "NAME_RESERVED" === e.code
                  ? console.error("This name has been reserved")
                  : "INVALID_NAME" === e.code
                    ? console.error("This name does not conform to the requirements")
                    : "INVALID_ATTESTATION" === e.code
                      ? console.error(
                          "Invalid attestation. Most likely caused by providing a public and private key that do not match.",
                        )
                      : console.error(e))
      }
      ;(y.destroy(), await l.destroy(), await d.close(), process.exit(u))
    },
  ),
  T = n(
    "reserve",
    l(
      "<registryPublicKey>",
      "Public RPC key of the keet-registry indexer to which to send the request",
    ),
    l("<username>", "Name to reserve"),
    l("<publicKey>", "Public key the name should point to"),
    l("<privateKey>", "Private key corresponding to the public key (in z32 encoding)"),
    async function ({ args: e, flags: o }) {
      const t = s.decode(e.registryPublicKey),
        r = s.decode(e.publicKey),
        a = g.decode(e.privateKey)
      if (64 !== a.byteLength) throw new Error("Invalid secret key")
      const n = { publicKey: r, secretKey: a },
        { username: c } = e,
        l = new i(),
        d = new p(l.dht),
        y = new f([t], d)
      let u = 0
      console.log("Reserving username with registry service...")
      try {
        const e = D(c, n)
        ;(await y.reserve(e),
          console.log(`Successfully requested to reserve ${c} -> ${s.normalize(r)}`))
      } catch (e) {
        ;((u = 1),
          "KEY_EXISTS" === e.code
            ? console.error(
                "This public key is already registered to another name (use the update command instead)",
              )
            : "NAME_EXISTS" === e.code
              ? console.error("This name is already taken")
              : "NAME_EXISTED" === e.code
                ? console.error("This name has been used before, so cannot be registered anymore")
                : "NAME_RESERVED" === e.code
                  ? console.error("This name has been reserved")
                  : "INVALID_NAME" === e.code
                    ? console.error("This name does not conform to the requirements")
                    : "INVALID_ATTESTATION" === e.code
                      ? console.error(
                          "Invalid attestation. Most likely caused by providing a public and private key that do not match.",
                        )
                      : console.error(e))
      }
      ;(y.destroy(), await l.destroy(), await d.close(), process.exit(u))
    },
  ),
  N = n(
    "update",
    d("Update the data of an existing entry"),
    l(
      "<registryPublicKey>",
      "Public RPC key of the keet-registry indexer to which to send the request",
    ),
    l("<name>", "Name to register"),
    l(
      "<clock>",
      "Value for the vector clock (normally set to the clock-value of the existing value plus 1)",
    ),
    l("<publicKey>", "Public key the name should point to"),
    l("<privateKey>", "Private key corresponding to the public key (in z32 encoding)"),
    async function ({ args: e, flags: o }) {
      const t = s.decode(e.registryPublicKey),
        r = s.decode(e.publicKey),
        a = parseInt(e.clock),
        n = g.decode(e.privateKey)
      if (64 !== n.byteLength) throw new Error("Invalid secret key")
      const c = { publicKey: r, secretKey: n },
        { name: l } = e,
        d = new i(),
        y = new f([t], d.dht)
      let u = 0
      console.log("Update record with keet-registry service...")
      try {
        ;(await y.attestAndPut(l, c, { clock: a }),
          console.log(`Successfully requested to update ${l} -> ${s.normalize(r)}`))
      } catch (e) {
        ;((u = 1),
          "UNKNOWN_KEY" === e.code
            ? console.error(
                "This public key is not yet registered to any record (use the register command instead)",
              )
            : "NAME_EXISTS" === e.code
              ? console.error("This name is already taken")
              : "OUTDATED_CLOCK" === e.code
                ? console.error(
                    "The vector clock was not higher than the current entry. Consider retrying with a higher clock value.",
                  )
                : "INVALID_NAME" === e.code
                  ? console.error("This name does not conform to the requirements")
                  : "INVALID_ATTESTATION" === e.code
                    ? console.error(
                        "Invalid attestation. Most likely caused by providing a public and private key that do not match.",
                      )
                    : "TOO_MANY_RECORDS_FOR_KEY" === e.code
                      ? console.error(
                          "This key pair has updated its name too many times. No new updates are allowed.",
                        )
                      : console.error(e))
      }
      ;(y.destroy(), await d.destroy(), process.exit(u))
    },
  ),
  I = n(
    "lookup",
    l("<registryKey>", "Public key of the keet registry's database (autobase view)"),
    l("<name>", "Name to lookup"),
    c("--storage|-s [path]", `storage path, defaults to ${w}`),
    c("--debug|-d", "Debug mode (more logs)"),
    c("--local-ok|-l", "Use the local database instead of waiting for a remote update"),
    async function ({ args: e, flags: o }) {
      const t = o.storage || w,
        { debug: r, localOk: a } = o,
        i = s.decode(e.registryKey),
        { name: n } = e
      n || (console.error("Name must be specified"), process.exit(1))
      const {
        client: c,
        store: l,
        swarm: d,
      } = await x(t, i, { debug: r, logger: console, localOk: a })
      console.log("Looking up...")
      const y = await c.get(n)
      ;(console.log("Result:"),
        console.log(O(y, { includeClock: !0 })),
        await c.close(),
        await d.destroy(),
        await l.close())
    },
  ),
  S = n(
    "key-lookup",
    l("<registryKey>", "Public key of the keet registry's database (autobase view)"),
    l("<key>", "key to lookup"),
    c("--storage|-s [path]", `storage path, defaults to ${w}`),
    c("--debug|-d", "Debug mode (more logs)"),
    c("--local-ok|-l", "Use the local database instead of waiting for a remote update"),
    async function ({ args: e, flags: o }) {
      const t = o.storage || w,
        { debug: r, localOk: a } = o,
        i = s.decode(e.registryKey),
        n = s.decode(e.key),
        { client: c, store: l, swarm: d } = await x(t, i, { debug: r, logger: console, localOk: a })
      console.log("Looking up..")
      const y = await c.getByKey(n)
      ;(console.log("Result:"),
        console.log(O(y)),
        await c.close(),
        await d.destroy(),
        await l.close())
    },
  ),
  q = n(
    "search",
    l("<registryKey>", "Public key of the keet registry's database (autobase view)"),
    l("<prefix>", "Prefix to look for (will return records starting with this prefix)"),
    c("--storage|-s [path]", `storage path, defaults to ${w}`),
    c("--debug|-d", "Debug mode (more logs)"),
    c("--local-ok|-l", "Use the local database instead of waiting for a remote update"),
    async function ({ args: e, flags: o }) {
      const t = o.storage || w,
        { debug: r, localOk: a } = o,
        i = s.decode(e.registryKey),
        { prefix: n } = e
      n || (console.error("Prefix must be specified"), process.exit(1))
      const {
        client: c,
        store: l,
        swarm: d,
      } = await x(t, i, { debug: r, logger: console, localOk: a })
      console.log(`Searching for records starting with ${n}...`)
      const y = await c.search(n).toArray()
      if (0 === y.length) console.log("No matching records found")
      else {
        console.log("Result:")
        for (const e of y) console.log(`- ${C(e)} -> ${s.normalize(M(e))}`)
      }
      ;(await c.close(), await d.destroy(), await l.close())
    },
  ),
  A = n(
    "tail",
    l("<registryKey>", "Public key of the name service's database (autobase view)"),
    c("--storage|-s [path]", `storage path, defaults to ${w}`),
    c("--local-ok|-l", "Use the local database instead of waiting for a remote update"),
    c(
      "--init-length [init-length]",
      "The initial db length from which to start watching (default 0)",
    ),
    async function ({ args: e, flags: o }) {
      const t = s.decode(e.registryKey),
        r = o.storage || "keet-registry-cli-tail",
        a = parseInt(o.initLength || 0),
        i = o.localOk,
        { client: n, swarm: c, store: l } = await x(r, t, { localOk: i, logger: console })
      ;(u(async () => {
        ;(console.info("Shutting down Keet registry tail service"),
          await n.close(),
          console.info("Destroying swarm"),
          await c.destroy(),
          console.log("swarm destroyed"),
          await l.close(),
          console.log("fully closed"))
      }),
        await n.ready(),
        console.log(`Observing changes, starting from length ${a}...`))
      try {
        for await (const { seq: e, value: o, type: t } of n.createRecordWatcher({ initLength: a }))
          "delete" === t
            ? console.log(`Seq ${e}: deleted name ${C(o)}`)
            : "insert" === t
              ? o.deleted
                ? console.log(`Seq ${e}: deleted name ${C(o)}`)
                : console.log(`Seq ${e}: inserted ${O(o)}`)
              : console.warn(
                  `Unexpected type: ${t}. Please contact the maintainer so it can be processed`,
                )
      } catch (e) {
        if (!n.closing) throw (console.error("unexpected error while tailing"), e)
      }
      console.log("done tailing")
    },
  )
function O(e, { includeClock: o = !1 } = {}) {
  return e
    ? e.deleted
      ? `[deleted record, no longer in use] ${C(e)} -> ${s.normalize(M(e))}${o ? " (timestamp: " + U(e) + ")" : ""}`
      : [`${C(e)} -> ${s.normalize(M(e))}${o ? " (timestamp: " + U(e) + ")" : ""}`].join("\n")
    : "Not found"
}
async function x(o, t, { debug: r, logger: a, localOk: n } = {}) {
  const c = new i(),
    l = new y(o)
  c.on("connection", (e, o) => {
    if ((l.replicate(e), r)) {
      const t = s.normalize(o.publicKey)
      ;(console.debug(`Opened connection to ${t}`),
        e.on("close", () => console.debug(`Closed connection to ${t}`)))
    }
  })
  const d = l.get({ key: t }),
    u = new m(d, { extension: !1 })
  return (
    await u.ready(),
    0 !== d.peers.length ||
      n ||
      (a.log(
        "Waiting for connection to the keet-registry service... (set the --local-ok flag to skip and use the local database)",
      ),
      c.join(d.discoveryKey),
      await e(d, "peer-add"),
      await d.update({ wait: !0 })),
    r && console.log(`Db version: ${d.length}`),
    { client: u, store: l, swarm: c }
  )
}
const _ = n("keet-registry-cli", $, T, N, I, S, q, A, v, P, E)
function D(e, o) {
  return { username: e, attestation: R(m.encodeReservation({ username: e }), o) }
}
function L(e, o) {
  const t = { username: e, timestamp: Date.now() }
  return { profile: t, attestation: R(k.encode(b, t), o) }
}
function R(e, o, t = null) {
  return { proof: h.attestData(e, o, t), memberKey: o.publicKey }
}
async function z(e) {
  let t = null
  try {
    t = await r.readFile(e, "utf-8")
  } catch (t) {
    if ("ENOENT" !== t.code) throw t
    await r.mkdir(o.dirname(e), { recursive: !0 })
    const i = a.keyPair(),
      n = { publicKey: s.normalize(i.publicKey), secretKey: g.encode(i.secretKey) }
    return (await r.writeFile(e, JSON.stringify(n), { encoding: "utf-8", mode: 384 }), i)
  }
  const { publicKey: i, secretKey: n } = await JSON.parse(t)
  return { publicKey: s.decode(i), secretKey: g.decode(n) }
}
function C(e) {
  return e.profile.username
}
function M(e) {
  return e.attestation.memberKey
}
function U(e) {
  return e.profile.timestamp
}
_.parse()