#!/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()