diff --git a/src/index.spec.ts b/src/index.spec.ts index e5bbf9c2..da946979 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,10 +1,24 @@ import 'dotenv/config' // TODO - test env +import { generatePrivateKey, getPublicKey } from 'nostr-tools'; import NewServer from '../proto/autogenerated/ts/express_server.js' import NewClient from '../proto/autogenerated/ts/http_client.js' import serverOptions from './auth.js'; import GetServerMethods from './services/serverMethods/index.js' import Main, { LoadMainSettingsFromEnv } from './services/main/index.js' import * as Types from '../proto/autogenerated/ts/types.js'; +import nostrMiddleware from './nostrMiddleware.js' +import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js'; +import NostrHandler from './services/nostr/index.js' + + +const settings = LoadNosrtSettingsFromEnv(true) + +const clientPrivateKey = generatePrivateKey() +const clientPublicKey = getPublicKey(Buffer.from(clientPrivateKey, "hex")) + +const serverPrivateKey = generatePrivateKey() +const serverPublicKey = getPublicKey(Buffer.from(serverPrivateKey, "hex")) + const testPort = 4000 var userAuthHeader = "" const client = NewClient({ @@ -16,9 +30,24 @@ const client = NewClient({ encryptCallback: async (b) => b, deviceId: "device0" }) +const clientNostr = new NostrHandler({ + allowedPubs: [], + privateKey: clientPrivateKey, + publicKey: clientPublicKey, + relays: settings.relays +}, (event, getContent) => { + console.log(getContent()) +}) const mainHandler = new Main(LoadMainSettingsFromEnv(true)) // TODO - test env file -const server = NewServer(GetServerMethods(mainHandler), { ...serverOptions(mainHandler), throwErrors: true }) -export const ignore = true +const serverMethods = GetServerMethods(mainHandler) +nostrMiddleware(serverMethods, mainHandler, { + allowedPubs: [clientPublicKey], + privateKey: serverPrivateKey, + publicKey: serverPublicKey, + relays: settings.relays +}) +const server = NewServer(serverMethods, { ...serverOptions(mainHandler), throwErrors: true }) +export const ignore = false export const setup = async () => { await mainHandler.storage.Connect() @@ -48,4 +77,7 @@ export default async (d: (message: string, failure?: boolean) => void) => { console.log(await client.NewAddress({ address_type: Types.AddressType.WITNESS_PUBKEY_HASH })) d("new address ok") + await new Promise(res => setTimeout(res, 2000)) + clientNostr.Send(serverPublicKey, JSON.stringify({ requestId: "a", method: '/api/user/chain/new', body: { address_type: 'WITNESS_PUBKEY_HASH' } })) + } diff --git a/src/index.ts b/src/index.ts index 90b967b9..f3ad4792 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,13 +3,16 @@ import NewServer from '../proto/autogenerated/ts/express_server' import GetServerMethods from './services/serverMethods' import serverOptions from './auth'; import Main, { LoadMainSettingsFromEnv } from './services/main' +import { LoadNosrtSettingsFromEnv } from './services/nostr' +import nostrMiddleware from './nostrMiddleware.js' const start = async () => { const mainHandler = new Main(LoadMainSettingsFromEnv()) await mainHandler.storage.Connect() const serverMethods = GetServerMethods(mainHandler) - + const nostrSettings = LoadNosrtSettingsFromEnv() + nostrMiddleware(serverMethods, mainHandler, nostrSettings) const Server = NewServer(serverMethods, serverOptions(mainHandler)) Server.Listen(3000) } diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts new file mode 100644 index 00000000..358bdf80 --- /dev/null +++ b/src/nostrMiddleware.ts @@ -0,0 +1,64 @@ +import Main from "./services/main/index.js" +import Nostr from "./services/nostr/index.js" +import { NostrSettings } from "./services/nostr/index.js" +import * as Types from '../proto/autogenerated/ts/types.js' +import GetServerMethods from './services/serverMethods/index.js' +import serverOptions from './auth.js'; +const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL +const handledRequests: string[] = [] // TODO: - big memory leak here, add TTL +type EventRequest = { + requestId: string + method: string + params: Record + body: any + query: Record +} + +export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings) => { + // TODO: - move to codegen + const nostr = new Nostr(nostrSettings, + async (event, getContent) => { + //@ts-ignore + const eventId = event.id + if (handledEvents.includes(eventId)) { + console.log("event already handled") + return + } + handledEvents.push(eventId) + const nostrPub = event.pubkey as string + if (!nostrSettings.allowedPubs.includes(nostrPub)) { + console.log("nostr pub not allowed") + return + } + let nostrUser = await mainHandler.storage.FindNostrUser(nostrPub) + if (!nostrUser) { + + nostrUser = await mainHandler.storage.AddNostrUser(nostrPub) + } + let j: EventRequest + try { + j = JSON.parse(getContent()) + } catch { + console.error("invalid json event received", event.content) + return + } + if (handledRequests.includes(j.requestId)) { + console.log("request already handled") + return + } + handledRequests.push(j.requestId) + switch (j.method) { + case '/api/user/chain/new': + const error = Types.NewAddressRequestValidate(j.body) + if (error !== null) { + console.error("invalid request from", nostrPub, j)// TODO: dont dox + return // TODO: respond + } + if (!serverMethods.NewAddress) { + throw new Error("unimplemented NewInvoice") + } + const res = await serverMethods.NewAddress({ user_id: nostrUser.user.user_id }, j.body) + nostr.Send(nostrPub, JSON.stringify({ ...res, requestId: j.requestId })) + } + }) +} \ No newline at end of file diff --git a/src/services/main/index.ts b/src/services/main/index.ts index b32fdfc5..61609dca 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -88,11 +88,11 @@ export default class { return (jwt.verify(token, this.settings.jwtSecret) as { userId: string }).userId } - async AddUser(req: Types.AddUserRequest): Promise { - const newUser = await this.storage.AddUser(req.name, req.callback_url, req.secret) + async AddBasicUser(req: Types.AddUserRequest): Promise { + const { user } = await this.storage.AddBasicUser(req.name, req.secret) return { - user_id: newUser.user_id, - auth_token: this.SignUserToken(newUser.user_id) + user_id: user.user_id, + auth_token: this.SignUserToken(user.user_id) } } @@ -164,4 +164,6 @@ export default class { } async OpenChannel(userId: string, req: Types.OpenChannelRequest): Promise { throw new Error("WIP") } + + } \ No newline at end of file diff --git a/src/services/nostr/index.spec.ts b/src/services/nostr/index.spec.ts index 9fe36d37..b1fc5667 100644 --- a/src/services/nostr/index.spec.ts +++ b/src/services/nostr/index.spec.ts @@ -4,37 +4,42 @@ import { generatePrivateKey, getPublicKey, relayPool } from 'nostr-tools' //@ts-ignore import { decrypt, encrypt } from 'nostr-tools/nip04.js' import NostrHandler, { LoadNosrtSettingsFromEnv, NostrSettings } from './index.js' +import { expect } from 'chai' +export const ignore = true const settings = LoadNosrtSettingsFromEnv(true) -const clientPool = relayPool() + const clientPrivateKey = generatePrivateKey() const clientPublicKey = getPublicKey(Buffer.from(clientPrivateKey, "hex")) -settings.privateKey = generatePrivateKey() -settings.publicKey = getPublicKey(Buffer.from(settings.privateKey, "hex")) -settings.allowedPubs = [clientPublicKey] - -const nostr = new NostrHandler(settings, async id => { console.log(id); return true }) -clientPool.setPrivateKey(clientPrivateKey) -export const setup = () => { - settings.relays.forEach(relay => { - try { - clientPool.addRelay(relay, { read: true, write: true }) - } catch (e) { - console.error("cannot add relay:", relay) - } - }); -} +const serverPrivateKey = generatePrivateKey() +const serverPublicKey = getPublicKey(Buffer.from(serverPrivateKey, "hex")) export default async (d: (message: string, failure?: boolean) => void) => { - const e = await clientPool.publish({ - content: encrypt(clientPrivateKey, settings.publicKey, "test"), - created_at: Math.floor(Date.now() / 1000), - kind: 4, - pubkey: clientPublicKey, - //@ts-ignore - tags: [['p', settings.publicKey]] - }, (status, url) => { - console.log(status, url) + + const clientNostr = new NostrHandler({ + allowedPubs: [], + privateKey: clientPrivateKey, + publicKey: clientPublicKey, + relays: settings.relays + }, (event, getContent) => { + }) + let receivedServerEvents = 0 + let latestReceivedServerEvent = "" + const serverNostr = new NostrHandler({ + allowedPubs: [clientPublicKey], + privateKey: serverPrivateKey, + publicKey: serverPublicKey, + relays: settings.relays + }, (event, getContent) => { + receivedServerEvents++ + latestReceivedServerEvent = getContent() + }) + await new Promise(res => setTimeout(res, 2000)) + clientNostr.Send(serverPublicKey, "test") + await new Promise(res => setTimeout(res, 1000)) + console.log(receivedServerEvents, latestReceivedServerEvent) + expect(receivedServerEvents).to.equal(1) + expect(latestReceivedServerEvent).to.equal("test") d("nostr ok") } \ No newline at end of file diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index bbedf054..06882c57 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -1,4 +1,4 @@ -import { relayPool, Subscription } from 'nostr-tools' +import { relayPool, Subscription, Event } from 'nostr-tools' //@ts-ignore import { decrypt, encrypt } from 'nostr-tools/nip04.js' import { EnvMustBeNonEmptyString } from '../helpers/envParser.js'; @@ -18,13 +18,11 @@ export const LoadNosrtSettingsFromEnv = (test = false): NostrSettings => { } } export default class { - shouldHandleEvent: (eventId: string) => Promise pool = relayPool() settings: NostrSettings sub: Subscription - constructor(settings: NostrSettings, shouldHandleCb: (eventId: string) => Promise) { + constructor(settings: NostrSettings, eventCallback: (event: Event, getContent: () => string) => void) { this.settings = settings - this.shouldHandleEvent = shouldHandleCb this.pool.setPrivateKey(settings.privateKey) settings.relays.forEach(relay => { try { @@ -36,13 +34,31 @@ export default class { this.sub = this.pool.sub({ //@ts-ignore filter: { + since: Math.ceil(Date.now() / 1000), kinds: [4], '#p': [settings.publicKey], - authors: settings.allowedPubs, }, - cb: async (event, relay) => { - console.log(decrypt(this.settings.privateKey, event.pubkey, event.content)) + cb: async (e, relay) => { + if (e.kind !== 4 || !e.pubkey) { + return + } + eventCallback(e, () => { + return decrypt(this.settings.privateKey, e.pubkey, e.content) + }) } }) } + + Send(nostrPub: string, message: string) { + this.pool.publish({ + content: encrypt(this.settings.privateKey, nostrPub, message), + created_at: Math.floor(Date.now() / 1000), + kind: 4, + pubkey: this.settings.publicKey, + //@ts-ignore + tags: [['p', nostrPub]] + }, (status, url) => { + console.log(status, url) // TODO + }) + } } \ No newline at end of file diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index b5bcd666..f1567fe5 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -15,7 +15,7 @@ export default (mainHandler: Main): Types.ServerMethods => { secret_CustomCheck: secret => secret.length >= 8 }) if (err != null) throw new Error(err.message) - return mainHandler.AddUser(req) + return mainHandler.AddBasicUser(req) }, AuthUser: async (ctx: Types.GuestContext, req: Types.AuthUserRequest): Promise => { throw new Error("unimplemented") diff --git a/src/services/storage/db.ts b/src/services/storage/db.ts index 10d27e17..a1e95cf3 100644 --- a/src/services/storage/db.ts +++ b/src/services/storage/db.ts @@ -7,6 +7,8 @@ import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js" import { UserInvoicePayment } from "./entity/UserInvoicePayment.js" import { EnvMustBeNonEmptyString } from "../helpers/envParser.js" import { UserTransactionPayment } from "./entity/UserTransactionPayment.js" +import { UserNostrAuth } from "./entity/UserNostrAuth.js" +import { UserBasicAuth } from "./entity/UserBasicAuth.js" export type DbSettings = { databaseFile: string } @@ -18,7 +20,7 @@ export default async (settings: DbSettings) => { type: "sqlite", database: settings.databaseFile, //logging: true, - entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment], + entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserNostrAuth, UserBasicAuth], synchronize: true }).initialize() } \ No newline at end of file diff --git a/src/services/storage/entity/User.ts b/src/services/storage/entity/User.ts index 5cb84543..c5911f86 100644 --- a/src/services/storage/entity/User.ts +++ b/src/services/storage/entity/User.ts @@ -10,16 +10,6 @@ export class User { @Index({ unique: true }) user_id: string - @Column() - @Index({ unique: true }) - name: string - - @Column() - secret_sha256: string - - @Column() - callbackUrl: string - @Column({ type: 'integer', default: 0 }) balance_sats: number diff --git a/src/services/storage/entity/UserBasicAuth.ts b/src/services/storage/entity/UserBasicAuth.ts new file mode 100644 index 00000000..f518e8e7 --- /dev/null +++ b/src/services/storage/entity/UserBasicAuth.ts @@ -0,0 +1,19 @@ +import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne } from "typeorm" +import { User } from "./User.js" + +@Entity() +export class UserBasicAuth { + + @PrimaryGeneratedColumn() + serial_id: number + + @ManyToOne(type => User) + user: User + + @Column() + @Index({ unique: true }) + name: string + + @Column() + secret_sha256: string +} diff --git a/src/services/storage/entity/UserNostrAuth.ts b/src/services/storage/entity/UserNostrAuth.ts new file mode 100644 index 00000000..4e486edf --- /dev/null +++ b/src/services/storage/entity/UserNostrAuth.ts @@ -0,0 +1,16 @@ +import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne } from "typeorm" +import { User } from "./User.js" + +@Entity() +export class UserNostrAuth { + + @PrimaryGeneratedColumn() + serial_id: number + + @ManyToOne(type => User) + user: User + + @Column() + @Index({ unique: true }) + nostr_pub: string +} diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 6546f3e5..80845f5d 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -7,6 +7,8 @@ import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js"; import { AddressReceivingTransaction } from "./entity/AddressReceivingTransaction.js"; import { UserInvoicePayment } from "./entity/UserInvoicePayment.js"; import { UserTransactionPayment } from "./entity/UserTransactionPayment.js"; +import { UserNostrAuth } from "./entity/UserNostrAuth.js"; +import { UserBasicAuth } from "./entity/UserBasicAuth.js"; export type StorageSettings = { dbSettings: DbSettings } @@ -26,15 +28,25 @@ export default class { StartTransaction(exec: (entityManager: EntityManager) => Promise) { return this.DB.transaction(exec) } - async AddUser(name: string, callbackUrl: string, secret: string, entityManager = this.DB): Promise { + async AddUser(entityManager = this.DB): Promise { + const newUser = entityManager.getRepository(User).create({ - user_id: crypto.randomBytes(32).toString('hex'), - name: name, - callbackUrl: callbackUrl, - secret_sha256: crypto.createHash('sha256').update(secret).digest('base64') + user_id: crypto.randomBytes(32).toString('hex') }) return entityManager.getRepository(User).save(newUser) } + async AddBasicUser(name: string, secret: string): Promise { + return this.DB.transaction(async tx => { + const user = await this.AddUser(tx) + const newUserAuth = tx.getRepository(UserBasicAuth).create({ + user: user, + name: name, + secret_sha256: crypto.createHash('sha256').update(secret).digest('base64') + }) + return tx.getRepository(UserBasicAuth).save(newUserAuth) + }) + + } FindUser(userId: string, entityManager = this.DB) { return entityManager.getRepository(User).findOne({ where: { @@ -50,6 +62,24 @@ export default class { return user } + async FindNostrUser(nostrPub: string, entityManager = this.DB): Promise { + return entityManager.getRepository(UserNostrAuth).findOne({ + where: { nostr_pub: nostrPub } + }) + + } + async AddNostrUser(nostrPub: string): Promise { + return this.DB.transaction(async tx => { + const user = await this.AddUser(tx) + const newAuth = tx.getRepository(UserNostrAuth).create({ + user: user, + nostr_pub: nostrPub + }) + return tx.getRepository(UserNostrAuth).save(newAuth) + }) + + } + async AddAddressReceivingTransaction(address: UserReceivingAddress, txHash: string, outputIndex: number, amount: number, serviceFee: number, entityManager = this.DB) { const newAddressTransaction = entityManager.getRepository(AddressReceivingTransaction).create({ user_address: address, @@ -154,4 +184,6 @@ export default class { throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing } } + + } \ No newline at end of file