initial version
This commit is contained in:
commit
ebec73a666
14 changed files with 546 additions and 0 deletions
6
.env.example
Normal file
6
.env.example
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
BASE_DOMAIN=npub.email
|
||||||
|
DB_URL=file:./users.db
|
||||||
|
SMTP_PORT=6587
|
||||||
|
HTTP_PORT=3000
|
||||||
|
LOG_FILE=/tmp/nostr-email.log
|
||||||
|
PUBLIC_API_BASE_URL=https://api.npub.email
|
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.idea/*
|
||||||
|
.vscode/*
|
||||||
|
/tmp
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
users.db
|
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
34
package.json
Normal file
34
package.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "mail-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "DEBUG='ndk:*' bun --watch src/index.ts",
|
||||||
|
"db:generate": "prisma generate",
|
||||||
|
"db:migrate": "prisma migrate dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@arx/utils": "git+ssh://git@git.arx-ccn.com:222/Arx/ts-utils#v0.0.4",
|
||||||
|
"@elysiajs/cors": "^1.1.1",
|
||||||
|
"@elysiajs/server-timing": "^1.1.0",
|
||||||
|
"@elysiajs/swagger": "^1.1.6",
|
||||||
|
"@libsql/client": "^0.14.0",
|
||||||
|
"@nostr-dev-kit/ndk": "^2.10.6",
|
||||||
|
"@prisma/adapter-libsql": "^5.22.0",
|
||||||
|
"@prisma/client": "5.22.0",
|
||||||
|
"elysia": "^1.1.25",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
|
"smtp-server": "^3.13.0",
|
||||||
|
"websocket-polyfill": "^1.0.0",
|
||||||
|
"winston": "^3.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node-forge": "^1.3.9",
|
||||||
|
"@types/smtp-server": "^3.5.10",
|
||||||
|
"bun-types": "latest",
|
||||||
|
"prisma": "5.22.0",
|
||||||
|
"typescript": "^5.3.2"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
29
prisma/migrations/20241125122247_init/migration.sql
Normal file
29
prisma/migrations/20241125122247_init/migration.sql
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"npub" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"registeredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"lastPayment" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"subscriptionDuration" INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "aliases" (
|
||||||
|
"npub" TEXT NOT NULL,
|
||||||
|
"alias" TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("npub", "alias"),
|
||||||
|
CONSTRAINT "aliases_npub_fkey" FOREIGN KEY ("npub") REFERENCES "users" ("npub") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "mail_queue" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"alias" TEXT NOT NULL,
|
||||||
|
"sender" TEXT NOT NULL,
|
||||||
|
"data" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "mail_queue_alias_fkey" FOREIGN KEY ("alias") REFERENCES "aliases" ("alias") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "aliases_alias_key" ON "aliases"("alias");
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `mail_queue` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropTable
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
DROP TABLE "mail_queue";
|
||||||
|
PRAGMA foreign_keys=on;
|
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
28
prisma/schema.prisma
Normal file
28
prisma/schema.prisma
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
previewFeatures = ["driverAdapters"]
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DB_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
npub String @id
|
||||||
|
registeredAt DateTime @default(now())
|
||||||
|
lastPayment DateTime @default(now())
|
||||||
|
subscriptionDuration Int?
|
||||||
|
aliases Alias[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Alias {
|
||||||
|
npub String
|
||||||
|
alias String @unique
|
||||||
|
user User @relation(fields: [npub], references: [npub])
|
||||||
|
|
||||||
|
@@id([npub, alias])
|
||||||
|
@@map("aliases")
|
||||||
|
}
|
247
src/httpServer.ts
Normal file
247
src/httpServer.ts
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
import {CashuMint, CashuWallet, getEncodedToken} from "@cashu/cashu-ts";
|
||||||
|
import {logger} from "./utils";
|
||||||
|
import * as nip98 from "nostr-tools/nip98";
|
||||||
|
import {Elysia, t} from "elysia";
|
||||||
|
import {swagger} from "@elysiajs/swagger";
|
||||||
|
import {serverTiming} from "@elysiajs/server-timing";
|
||||||
|
import {PrismaClient} from "@prisma/client";
|
||||||
|
import {TokenInfoWithMailSubscriptionDuration} from "@arx/utils/cashu.ts";
|
||||||
|
import {npubToPubKeyString, pubKeyStringToNpub} from "@arx/utils/nostr.ts";
|
||||||
|
import cors from "@elysiajs/cors";
|
||||||
|
|
||||||
|
const npubType = t.String({
|
||||||
|
pattern: `^npub1[023456789acdefghjklmnpqrstuvwxyz]{58}$`,
|
||||||
|
error: 'Invalid npub format'
|
||||||
|
});
|
||||||
|
|
||||||
|
const cashuTokenType = t.String({
|
||||||
|
pattern: '^cashu[A-Za-z0-9+-_]*={0,3}$',
|
||||||
|
error: 'Invalid Cashu token format'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class HttpServer {
|
||||||
|
constructor(private db: PrismaClient, port: number) {
|
||||||
|
new Elysia()
|
||||||
|
.use(swagger({
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: 'npub.email Documentation',
|
||||||
|
version: '0.0.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.use(serverTiming())
|
||||||
|
.use(cors())
|
||||||
|
.get('/', 'nostr.email server')
|
||||||
|
.get('/subscription/:npub', this.getSubscriptionForNpub, {
|
||||||
|
params: t.Object({
|
||||||
|
npub: npubType
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/aliases/:npub', this.getAliasesForNpub, {
|
||||||
|
params: t.Object({
|
||||||
|
npub: npubType,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.get('/alias/:alias', this.getNpubForAlias, {
|
||||||
|
params: t.Object({
|
||||||
|
alias: t.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.post('/addAlias', this.addAlias, {
|
||||||
|
body: t.Object({
|
||||||
|
alias: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/addTime/:npub', this.addTimeToNpub, {
|
||||||
|
params: t.Object({
|
||||||
|
npub: npubType,
|
||||||
|
}),
|
||||||
|
body: t.Object({
|
||||||
|
tokenString: cashuTokenType
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.listen(port)
|
||||||
|
logger.info(`HTTP Server running on port ${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscriptionForNpub = async ({params: {npub}}: {
|
||||||
|
params: {
|
||||||
|
npub: string
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const user = await this.db.user.findFirst({
|
||||||
|
where: {
|
||||||
|
npub
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
aliases: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) return {
|
||||||
|
subscribed: false
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
subscribed: true,
|
||||||
|
subscribedUntil: user.subscriptionDuration == null ? Infinity : Math.floor(user.lastPayment.getTime() / 1000) + user.subscriptionDuration
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getNpubForAlias = async ({params: {alias}}: {
|
||||||
|
params: {
|
||||||
|
alias: string
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const user = await this.db.user.findFirst({
|
||||||
|
where: {
|
||||||
|
aliases: {
|
||||||
|
some: {
|
||||||
|
alias
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) return new Response('Not found', {
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
return user.npub;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAliasesForNpub = async ({params: {npub}, headers}: {
|
||||||
|
params: {
|
||||||
|
npub: string
|
||||||
|
},
|
||||||
|
headers: Record<string, string | undefined>
|
||||||
|
}) => {
|
||||||
|
const unpacked = await this.getUnpackedAuthHeader(headers, `/aliases/${npub}`);
|
||||||
|
const npubAsPubkey = npubToPubKeyString(npub);
|
||||||
|
if (unpacked.pubkey !== npubAsPubkey)
|
||||||
|
return new Response('Unauthorized', {
|
||||||
|
status: 401
|
||||||
|
})
|
||||||
|
const user = await this.db.user.findFirst({
|
||||||
|
where: {
|
||||||
|
npub
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
aliases: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) return new Response('Not found', {
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
return user.aliases.map(alias => alias.alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAlias = async ({body: {alias}, headers}: {
|
||||||
|
body: {
|
||||||
|
alias: string
|
||||||
|
},
|
||||||
|
headers: Record<string, string | undefined>
|
||||||
|
}) => {
|
||||||
|
const unpacked = await this.getUnpackedAuthHeader(headers, '/addAlias');
|
||||||
|
const unpackedKeyToNpub = pubKeyStringToNpub(unpacked.pubkey);
|
||||||
|
const userInDb = await this.db.user.findFirst({
|
||||||
|
where: {
|
||||||
|
npub: unpackedKeyToNpub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!userInDb) return new Response('Unauthorized', {
|
||||||
|
status: 401
|
||||||
|
});
|
||||||
|
|
||||||
|
const stillHasSubscription = userInDb.subscriptionDuration === null || Math.floor(userInDb.lastPayment.getTime() / 1000) + userInDb.subscriptionDuration > Date.now() / 1000;
|
||||||
|
if (!stillHasSubscription) return new Response('User has no subscription', {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
const aliasInDb = await this.db.alias.findFirst({
|
||||||
|
where: {
|
||||||
|
alias
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (aliasInDb) return new Response('Alias already exists', {
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
return this.db.user.update({
|
||||||
|
where: {
|
||||||
|
npub: unpackedKeyToNpub
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
aliases: {
|
||||||
|
create: {
|
||||||
|
alias
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addTimeToNpub = async ({params: {npub}, body: {tokenString}}: {
|
||||||
|
params: {
|
||||||
|
npub: string
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
tokenString: string
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const userInDb = await this.db.user.findFirst({
|
||||||
|
where: {
|
||||||
|
npub
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userInDb && (userInDb.subscriptionDuration === null || userInDb.subscriptionDuration === -1))
|
||||||
|
return new Response('User has unlimited subscription', {
|
||||||
|
status: 400
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenInfo = new TokenInfoWithMailSubscriptionDuration(tokenString);
|
||||||
|
const mint = new CashuMint(tokenInfo.mint);
|
||||||
|
const wallet = new CashuWallet(mint);
|
||||||
|
const newToken = await wallet.receive(tokenString);
|
||||||
|
const encodedToken = getEncodedToken({
|
||||||
|
token: [{
|
||||||
|
mint: tokenInfo.mint,
|
||||||
|
proofs: newToken
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
logger.info(`New cashu token: ${encodedToken}`);
|
||||||
|
if (userInDb) {
|
||||||
|
let timeRemaining = Math.max(0, Math.floor((+new Date(userInDb.lastPayment.getTime() + userInDb.subscriptionDuration! * 1000) - +new Date()) / 1000));
|
||||||
|
timeRemaining += tokenInfo.duration;
|
||||||
|
await this.db.user.update({
|
||||||
|
where: {
|
||||||
|
npub
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
lastPayment: new Date(),
|
||||||
|
subscriptionDuration: timeRemaining
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
newTimeRemaining: timeRemaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.db.user.create({
|
||||||
|
data: {
|
||||||
|
npub,
|
||||||
|
registeredAt: new Date(),
|
||||||
|
lastPayment: new Date(),
|
||||||
|
subscriptionDuration: tokenInfo.duration
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
newTimeRemaining: tokenInfo.duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUnpackedAuthHeader = async (headers: Record<string, string | undefined>, url: string) => {
|
||||||
|
if (!headers.authorization)
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
const authHeader = headers.authorization.split(' ')[1];
|
||||||
|
const validate = await nip98.validateToken(authHeader, `${process.env.PUBLIC_API_BASE_URL!}${url}`, "POST");
|
||||||
|
if (!validate)
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
return await nip98.unpackEventFromToken(authHeader);
|
||||||
|
}
|
||||||
|
}
|
24
src/index.ts
Normal file
24
src/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {createClient as createLibSQLClient} from "@libsql/client";
|
||||||
|
import "websocket-polyfill";
|
||||||
|
import {PrismaClient} from "@prisma/client";
|
||||||
|
import {PrismaLibSQL} from "@prisma/adapter-libsql";
|
||||||
|
import {NostrSmtpServer} from "./smtpServer";
|
||||||
|
import {HttpServer} from "./httpServer";
|
||||||
|
|
||||||
|
if (!process.env.BASE_DOMAIN)
|
||||||
|
throw new Error("BASE_DOMAIN is not set");
|
||||||
|
if (!process.env.DB_URL)
|
||||||
|
throw new Error("DB_URL is not set");
|
||||||
|
if (!process.env.PUBLIC_API_BASE_URL)
|
||||||
|
throw new Error("PUBLIC_API_BASE_URL is not set");
|
||||||
|
|
||||||
|
const dbClient = createLibSQLClient({
|
||||||
|
url: process.env.DB_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new PrismaClient({
|
||||||
|
adapter: new PrismaLibSQL(dbClient)
|
||||||
|
});
|
||||||
|
|
||||||
|
new NostrSmtpServer(db, parseInt(process.env.SMTP_PORT || '6587'));
|
||||||
|
new HttpServer(db, parseInt(process.env.HTTP_PORT || '3000'));
|
100
src/smtpServer.ts
Normal file
100
src/smtpServer.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import {SMTPServer} from "smtp-server";
|
||||||
|
import {getNDK} from "./utils";
|
||||||
|
import {generateSecretKey} from "nostr-tools";
|
||||||
|
import {NDKEvent, NDKKind, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk";
|
||||||
|
import {PrismaClient} from "@prisma/client";
|
||||||
|
import {logger} from "./utils/logs";
|
||||||
|
import {encryptEventForRecipient, parseEmail} from "@arx/utils";
|
||||||
|
|
||||||
|
export class NostrSmtpServer {
|
||||||
|
private server: SMTPServer;
|
||||||
|
|
||||||
|
constructor(db: PrismaClient, port: number) {
|
||||||
|
this.server = new SMTPServer({
|
||||||
|
authOptional: true,
|
||||||
|
logger: false,
|
||||||
|
|
||||||
|
onData: (stream, session, callback) => {
|
||||||
|
let mailData = '';
|
||||||
|
|
||||||
|
stream.on('data', (chunk: Buffer) => {
|
||||||
|
mailData += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', async () => {
|
||||||
|
if (!session.envelope.mailFrom) {
|
||||||
|
logger.warn('Ignoring email without sender');
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsedEmail = parseEmail(mailData);
|
||||||
|
for (let recipientEmail of session.envelope.rcptTo) {
|
||||||
|
const address = recipientEmail.address;
|
||||||
|
const parts = address.split('@');
|
||||||
|
if (parts[1] !== process.env.BASE_DOMAIN) {
|
||||||
|
logger.warn('Not sending email to', address, 'because it is not in the allowed domain');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const alias = parts[0];
|
||||||
|
const user = await db.alias.findUnique({
|
||||||
|
where: {
|
||||||
|
alias
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
logger.warn('No user found for', alias, 'skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const timeRemainingInSubscription = user.user.subscriptionDuration === null ? Infinity : (user.user.subscriptionDuration * 1000) - Date.now() + user.user.lastPayment.getTime();
|
||||||
|
if (timeRemainingInSubscription <= 0) {
|
||||||
|
logger.warn(`Subscription has expired for ${alias}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const recipient = user.npub;
|
||||||
|
const randomKey = generateSecretKey();
|
||||||
|
const randomKeySinger = new NDKPrivateKeySigner(randomKey);
|
||||||
|
const ndk = getNDK();
|
||||||
|
ndk.signer = randomKeySinger;
|
||||||
|
await ndk.connect();
|
||||||
|
const ndkUser = ndk.getUser({
|
||||||
|
npub: recipient
|
||||||
|
});
|
||||||
|
const randomKeyUser = await randomKeySinger.user();
|
||||||
|
const event = new NDKEvent();
|
||||||
|
event.kind = NDKKind.Article;
|
||||||
|
event.content = parsedEmail.body;
|
||||||
|
event.created_at = Math.floor(Date.now() / 1000);
|
||||||
|
event.pubkey = randomKeyUser.pubkey;
|
||||||
|
event.tags.push(['p', ndkUser.pubkey])
|
||||||
|
event.tags.push(['subject', parsedEmail.subject]);
|
||||||
|
event.tags.push(['email:localIP', session.localAddress]);
|
||||||
|
event.tags.push(['email:remoteIP', session.remoteAddress]);
|
||||||
|
event.tags.push(['email:isEmail', 'true']);
|
||||||
|
for (let to of session.envelope.rcptTo)
|
||||||
|
event.tags.push(['email:to', to.address]);
|
||||||
|
for (let header of Object.keys(parsedEmail.headers))
|
||||||
|
event.tags.push([`email:header:${header}`, parsedEmail.headers[header]]);
|
||||||
|
event.tags.push(['email:session', session.id]);
|
||||||
|
event.tags.push(['email:from', session.envelope.mailFrom?.address ?? '']);
|
||||||
|
|
||||||
|
await event.sign(randomKeySinger);
|
||||||
|
const encryptedEvent = await encryptEventForRecipient(ndk, event, ndkUser);
|
||||||
|
await encryptedEvent.publish();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(JSON.stringify(e));
|
||||||
|
} finally {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.listen(port, '0.0.0.0');
|
||||||
|
logger.info(`SMTP Server running on port ${port}`);
|
||||||
|
}
|
||||||
|
}
|
16
src/utils/index.ts
Normal file
16
src/utils/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import NDK from "@nostr-dev-kit/ndk";
|
||||||
|
|
||||||
|
export * from "./logs";
|
||||||
|
|
||||||
|
export function getNDK() {
|
||||||
|
return new NDK({
|
||||||
|
explicitRelayUrls: [
|
||||||
|
'wss://relay.primal.net',
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://relay.nostr.band',
|
||||||
|
'wss://offchain.pub'
|
||||||
|
],
|
||||||
|
autoConnectUserRelays: false,
|
||||||
|
enableOutboxModel: true,
|
||||||
|
});
|
||||||
|
}
|
23
src/utils/logs.ts
Normal file
23
src/utils/logs.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import winston from "winston";
|
||||||
|
|
||||||
|
const {combine, timestamp, printf, align, colorize, json} = winston.format;
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
level: 'info',
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: combine(
|
||||||
|
colorize({all: true}),
|
||||||
|
timestamp({
|
||||||
|
format: 'YYYY-MM-DD hh:mm:ss.SSS A',
|
||||||
|
}),
|
||||||
|
align(),
|
||||||
|
printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: process.env.LOG_FILE || '/tmp/nostr-email.log',
|
||||||
|
format: combine(timestamp(), json()),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "preserve",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue