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…
	
	Add table
		Add a link
		
	
		Reference in a new issue