Add /name and nostr.json methods

This commit is contained in:
artur 2024-02-05 13:50:08 +03:00
parent f41f62f735
commit 078bcb03bc
5 changed files with 172 additions and 6 deletions

View File

@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "Names" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"npub" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Names_name_key" ON "Names"("name");

View File

@ -0,0 +1,20 @@
/*
Warnings:
- Added the required column `timestamp` to the `Names` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Names" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"npub" TEXT NOT NULL,
"timestamp" BIGINT NOT NULL
);
INSERT INTO "new_Names" ("id", "name", "npub") SELECT "id", "name", "npub" FROM "Names";
DROP TABLE "Names";
ALTER TABLE "new_Names" RENAME TO "Names";
CREATE UNIQUE INDEX "Names_name_key" ON "Names"("name");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -27,3 +27,10 @@ model NpubData {
pwh2 String pwh2 String
salt String salt String
} }
model Names {
id Int @id @default(autoincrement())
name String @unique
npub String
timestamp BigInt
}

View File

@ -15,6 +15,23 @@ async function makePwh2(pwh, salt) {
}) })
} }
function countLeadingZeros(hex) {
let count = 0;
for (let i = 0; i < hex.length; i++) {
const nibble = parseInt(hex[i], 16);
if (nibble === 0) {
count += 4;
} else {
count += Math.clz32(nibble) - 28;
break;
}
}
return count;
}
module.exports = { module.exports = {
makePwh2 makePwh2,
countLeadingZeros
} }

View File

@ -5,7 +5,7 @@ const { createHash } = require('node:crypto');
const express = require("express"); const express = require("express");
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const { nip19 } = require('nostr-tools') const { nip19 } = require('nostr-tools')
const { makePwh2 } = require('./crypto'); const { makePwh2, countLeadingZeros } = require('./crypto');
const { PrismaClient } = require('@prisma/client') const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient() const prisma = new PrismaClient()
@ -335,7 +335,6 @@ function processRelayQueue() {
for (const url of uniqRelays.values()) { for (const url of uniqRelays.values()) {
const r = relays.get(url); const r = relays.get(url);
console.log(new Date(), "update relay", url, "sub", r.subQueue.length, "unsub", r.unsubQueue.length); console.log(new Date(), "update relay", url, "sub", r.subQueue.length, "unsub", r.unsubQueue.length);
console.log("old subs", r.subs, "unsubQueue", r.unsubQueue, "subQueue", r.subQueue)
// first handle the unsubs // first handle the unsubs
for (const p of new Set(r.unsubQueue).values()) { for (const p of new Set(r.unsubQueue).values()) {
@ -372,7 +371,6 @@ function processRelayQueue() {
// store NDK sub itself // store NDK sub itself
r.subs.set(sub.subId, sub); r.subs.set(sub.subId, sub);
} }
console.log("new subs", r.subs)
} }
// close old subs after new subs have activated // close old subs after new subs have activated
@ -396,8 +394,23 @@ function digest(algo, data) {
return hash.digest('hex'); return hash.digest('hex');
} }
function isValidName(name) {
const REGEX = /^[a-z0-9_]{3,128}$/
return REGEX.test(name)
}
function getMinPow(name, req) {
let minPow = 14
if (name.length <= 6) {
minPow += 4
}
// FIXME check IP rate limits
return minPow
}
// nip98 // nip98
async function verifyAuthNostr(req, npub, path) { async function verifyAuthNostr(req, npub, path, minPow = 0) {
try { try {
const { type, data: pubkey } = nip19.decode(npub); const { type, data: pubkey } = nip19.decode(npub);
if (type !== 'npub') return false; if (type !== 'npub') return false;
@ -416,10 +429,15 @@ async function verifyAuthNostr(req, npub, path) {
if (event.pubkey !== pubkey) return false; if (event.pubkey !== pubkey) return false;
if (event.kind !== 27235) return false; if (event.kind !== 27235) return false;
if (event.created_at < (now - 60) || event.created_at > (now + 60)) return false; if (event.created_at < (now - 60) || event.created_at > (now + 60)) return false;
const pow = countLeadingZeros(event.id);
console.log("pow", pow, "min", minPow, "id", event.id);
if (minPow && pow < minPow) return false;
const u = event.tags.find(t => t.length === 2 && t[0] === 'u')?.[1] const u = event.tags.find(t => t.length === 2 && t[0] === 'u')?.[1]
const method = event.tags.find(t => t.length === 2 && t[0] === 'method')?.[1] const method = event.tags.find(t => t.length === 2 && t[0] === 'method')?.[1]
const payload = event.tags.find(t => t.length === 2 && t[0] === 'payload')?.[1] const payload = event.tags.find(t => t.length === 2 && t[0] === 'payload')?.[1]
// console.log({ u, method, payload }) if (method !== req.method) return false;
const url = new URL(u) const url = new URL(u)
// console.log({ url }) // console.log({ url })
@ -653,6 +671,101 @@ app.post(GET_PATH, async (req, res) => {
} }
}) })
const NAME_PATH = '/name'
app.post(NAME_PATH, async (req, res) => {
try {
const { npub, name } = req.body;
if (!isValidName(name)) {
console.log("invalid name", name)
res.status(400).send({
error: `Bad name`
})
return
}
const { type } = nip19.decode(npub);
if (type !== 'npub') {
console.log("bad npub", npub)
res.status(400).send({
error: 'Bad npub'
})
return
}
const minPow = getMinPow(name, req)
if (!await verifyAuthNostr(req, npub, NAME_PATH, minPow)) {
console.log("auth failed", npub)
res.status(403).send({
error: `Bad auth`,
minPow
})
return
}
try {
const dbr = await prisma.names.create({
data: {
npub,
name,
timestamp: Date.now()
}
});
console.log({ dbr });
} catch (e) {
res.status(400).send({
error: 'Name taken'
})
return
}
// reply ok
res
.status(201)
.send({
ok: true
});
} catch (e) {
console.log(new Date(), "error req from ", req.ip, e.toString())
res.status(400).send({
error: "Internal error"
});
}
})
const JSON_PATH = '/.well-known/nostr.json'
app.get(JSON_PATH, async (req, res) => {
try {
const { name } = req.query;
const rec = await prisma.names.findUnique({
where: {
name
}
})
console.log("name", name, rec);
const data = {
names: {
}
}
if (rec) {
const { data: pubkey } = nip19.decode(rec.npub)
data.names[rec.name] = pubkey
}
res
.status(200)
.send(data);
} catch (e) {
console.log(new Date(), "error req from ", req.ip, e.toString())
res.status(400).send({
"error": e.toString()
});
}
})
async function loadFromDb() { async function loadFromDb() {
const start = Date.now() const start = Date.now()