Add /name and nostr.json methods
This commit is contained in:
parent
f41f62f735
commit
078bcb03bc
9
prisma/migrations/20240205093445_add_names/migration.sql
Normal file
9
prisma/migrations/20240205093445_add_names/migration.sql
Normal 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");
|
|
@ -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;
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
123
src/index.js
123
src/index.js
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user