Skip to content

Commit bbc4ed2

Browse files
chore: remove 5 dependencies by inlining their functionality
- bech32: inline encode/decode in transform.ts (~75 lines) - accepts: replace with request.headers.accept?.includes() check - tor-control-ts: inline Tor control protocol via net.Socket (TorClient class) - helmet: inline Content-Security-Policy header construction - dotenv: use Node.js --env-file-if-exists flag in npm scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3f8f148 commit bbc4ed2

11 files changed

Lines changed: 186 additions & 89 deletions

File tree

package.json

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
],
2525
"main": "src/index.ts",
2626
"scripts": {
27-
"dev": "node -r ts-node/register src/index.ts",
28-
"clean-db": "node -r ts-node/register src/clean-db.ts",
27+
"dev": "node --env-file-if-exists=.env -r ts-node/register src/index.ts",
28+
"clean-db": "node --env-file-if-exists=.env -r ts-node/register src/clean-db.ts",
2929
"clean": "rimraf ./{dist,.nyc_output,.test-reports,.coverage}",
3030
"build": "tsc --project tsconfig.build.json",
3131
"prestart": "npm run build",
32-
"start": "cd dist && node src/index.js",
32+
"start": "cd dist && node --env-file-if-exists=../.env src/index.js",
3333
"build:check": "npm run build -- --noEmit",
3434
"knip": "knip --config .knip.json --production --include files,dependencies --no-progress --reporter compact",
3535
"lint": "biome lint ./src ./test",
@@ -38,7 +38,7 @@
3838
"lint:fix": "npm run lint -- --write",
3939
"format": "biome format --write ./src ./test",
4040
"format:check": "biome format ./src ./test",
41-
"import": "node -r ts-node/register src/import-events.ts",
41+
"import": "node --env-file-if-exists=.env -r ts-node/register src/import-events.ts",
4242
"db:migrate": "knex migrate:latest",
4343
"db:migrate:rollback": "knex migrate:rollback",
4444
"db:seed": "knex seed:run",
@@ -50,7 +50,7 @@
5050
"pretest:integration": "mkdir -p .test-reports/integration",
5151
"test:integration": "cucumber-js",
5252
"cover:integration": "nyc --report-dir .coverage/integration npm run test:integration -- -p cover",
53-
"export": "node -r ts-node/register src/scripts/export-events.ts",
53+
"export": "node --env-file-if-exists=.env -r ts-node/register src/scripts/export-events.ts",
5454
"docker:compose:start": "./scripts/start",
5555
"docker:compose:stop": "./scripts/stop",
5656
"docker:compose:clean": "./scripts/clean",
@@ -124,20 +124,15 @@
124124
},
125125
"dependencies": {
126126
"@noble/secp256k1": "1.7.1",
127-
"accepts": "^1.3.8",
128127
"axios": "^1.15.0",
129-
"bech32": "2.0.0",
130128
"debug": "4.3.4",
131-
"dotenv": "16.0.3",
132129
"express": "4.22.1",
133-
"helmet": "6.0.1",
134130
"js-yaml": "4.1.1",
135131
"knex": "2.4.2",
136132
"pg": "8.9.0",
137133
"pg-query-stream": "4.3.0",
138134
"ramda": "0.28.0",
139135
"redis": "4.5.1",
140-
"tor-control-ts": "^1.0.0",
141136
"ws": "^8.18.0",
142137
"zod": "^3.22.4"
143138
},

src/clean-db.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { createInterface } from 'readline'
2-
import dotenv from 'dotenv'
32
import { Knex } from 'knex'
43

54
import { getMasterDbClient } from './database/client'
65

7-
dotenv.config()
8-
96
type CleanDbOptions = {
107
all: boolean
118
dryRun: boolean

src/factories/web-app-factory.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import express from 'express'
2-
import helmet from 'helmet'
32
import { randomBytes } from 'crypto'
43

54
import { createSettings } from './settings-factory'
@@ -28,7 +27,11 @@ export const createWebApp = () => {
2827
'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
2928
}
3029

31-
return helmet.contentSecurityPolicy({ directives })(req, res, next)
30+
const csp = Object.entries(directives)
31+
.map(([key, values]) => `${key} ${values.join(' ')}`)
32+
.join('; ')
33+
res.setHeader('Content-Security-Policy', csp)
34+
return next()
3235
})
3336
.use('/favicon.ico', express.static('./resources/favicon.ico'))
3437
.use('/css', express.static('./resources/css'))

src/handlers/request-handlers/root-request-handler.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { NextFunction, Request, Response } from 'express'
22
import { path, pathEq } from 'ramda'
3-
import accepts from 'accepts'
43
import { createSettings } from '../../factories/settings-factory'
54
import { escapeHtml } from '../../utils/html'
65
import { FeeSchedule } from '../../@types/settings'
@@ -11,7 +10,7 @@ import packageJson from '../../../package.json'
1110
export const rootRequestHandler = (request: Request, response: Response, next: NextFunction) => {
1211
const settings = createSettings()
1312

14-
if (accepts(request).type(['application/nostr+json'])) {
13+
if (request.headers.accept?.includes('application/nostr+json')) {
1514
const {
1615
info: { name, description, pubkey: rawPubkey, contact, relay_url },
1716
} = settings

src/import-events.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import { extname, resolve } from 'path'
22

33
import fs from 'fs'
44

5-
import dotenv from 'dotenv'
6-
7-
dotenv.config()
8-
95
import {
106
createEventBatchPersister,
117
EventImportLineError,

src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import cluster from 'cluster'
2-
import dotenv from 'dotenv'
3-
dotenv.config()
42

53
import { appFactory } from './factories/app-factory'
64
import { maintenanceWorkerFactory } from './factories/maintenance-worker-factory'

src/routes/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import accepts from 'accepts'
21
import express from 'express'
32

43
import { nodeinfo21Handler, nodeinfoHandler } from '../handlers/request-handlers/nodeinfo-handler'
@@ -14,7 +13,7 @@ import { rootRequestHandler } from '../handlers/request-handlers/root-request-ha
1413
const router = express.Router()
1514

1615
router.use((req, res, next) => {
17-
if (req.method === 'GET' && accepts(req).type(['application/nostr+json'])) {
16+
if (req.method === 'GET' && req.headers.accept?.includes('application/nostr+json')) {
1817
return rootRequestHandler(req, res, next)
1918
}
2019
next()

src/scripts/export-events.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import 'pg-query-stream'
2-
import dotenv from 'dotenv'
3-
dotenv.config()
42

53
import fs from 'fs'
64
import path from 'path'

src/tor/client.ts

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import net from 'net'
12
import { readFile, writeFile } from 'fs/promises'
23
import { homedir } from 'os'
34
import { join } from 'path'
4-
import { Tor } from 'tor-control-ts'
55

66
import { createLogger } from '../factories/logger-factory'
77
import { TorConfig } from '../@types/tor'
@@ -24,7 +24,75 @@ export const createTorConfig = (): TorConfig => {
2424
}
2525
}
2626

27-
let client: Tor | undefined
27+
type OnionResult = { ServiceID?: string; PrivateKey?: string }
28+
29+
export class TorClient {
30+
private socket: net.Socket | undefined
31+
private readonly host: string
32+
private readonly port: number
33+
private readonly password: string
34+
35+
constructor({ host, port, password }: { host?: string; port?: number; password?: string } = {}) {
36+
this.host = host ?? 'localhost'
37+
this.port = port ?? 9051
38+
this.password = password ?? ''
39+
}
40+
41+
connect(): Promise<void> {
42+
return new Promise((resolve, reject) => {
43+
this.socket = net.connect({ host: this.host, port: this.port })
44+
this.socket.once('error', reject)
45+
this.socket.once('data', (data) => {
46+
if (/^250/.test(data.toString())) { resolve() }
47+
else { reject(new Error(`Tor auth failed: ${data}`)) }
48+
})
49+
this.socket.write(`AUTHENTICATE "${this.password}"\r\n`)
50+
})
51+
}
52+
53+
private sendCommand(command: string): Promise<string> {
54+
return new Promise((resolve, reject) => {
55+
if (!this.socket) {
56+
reject(new Error('Not connected to Tor control port'))
57+
return
58+
}
59+
let buf = ''
60+
const onData = (data: Buffer) => {
61+
buf += data.toString()
62+
if (buf.endsWith('\r\n')) {
63+
this.socket!.off('data', onData)
64+
if (/^250/.test(buf)) { resolve(buf) }
65+
else { reject(new Error(buf.trim())) }
66+
}
67+
}
68+
this.socket.on('data', onData)
69+
this.socket.write(`${command}\r\n`)
70+
})
71+
}
72+
73+
async addOnion(port: number, host?: string, privateKey?: string | null): Promise<OnionResult> {
74+
const key = privateKey ?? 'NEW:BEST'
75+
const portSpec = host !== undefined ? `${port},${host}:${port}` : `${port}`
76+
const response = await this.sendCommand(`ADD_ONION ${key} Port=${portSpec}`)
77+
78+
const result: OnionResult = {}
79+
for (const line of response.split('\r\n')) {
80+
const m = line.match(/^250[-\s](\w+)=(.+)$/)
81+
if (m) { (result as Record<string, string>)[m[1]] = m[2] }
82+
}
83+
if (result.ServiceID) { result.ServiceID += '.onion' }
84+
if (!result.PrivateKey && privateKey) { result.PrivateKey = privateKey }
85+
return result
86+
}
87+
88+
async quit(): Promise<void> {
89+
await this.sendCommand('QUIT').catch(() => undefined)
90+
this.socket?.destroy()
91+
this.socket = undefined
92+
}
93+
}
94+
95+
let client: TorClient | undefined
2896

2997
export const getTorClient = async () => {
3098
if (!client) {
@@ -33,7 +101,7 @@ export const getTorClient = async () => {
33101

34102
if (config.host !== undefined) {
35103
debug('connecting')
36-
client = new Tor(config)
104+
client = new TorClient(config)
37105
try{
38106
await client.connect()
39107
}catch(_error){
@@ -81,7 +149,7 @@ export const addOnion = async (
81149
debug('saving private key to %s', path)
82150

83151
await writeFile(path, hiddenService.PrivateKey, 'utf8')
84-
return hiddenService.ServiceID
152+
return hiddenService.ServiceID!
85153
}else{
86154
throw new Error(JSON.stringify(hiddenService))
87155
}

src/utils/transform.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,83 @@
11
import { always, applySpec, cond, equals, ifElse, is, isNil, multiply, path, pathSatisfies, pipe, prop, propSatisfies, T } from 'ramda'
2-
import { bech32 } from 'bech32'
32

43
import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice'
54
import { User } from '../@types/user'
65

6+
const BECH32_ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
7+
const BECH32_ALPHABET_MAP: Record<string, number> = {}
8+
for (let i = 0; i < BECH32_ALPHABET.length; i++) { BECH32_ALPHABET_MAP[BECH32_ALPHABET[i]] = i }
9+
10+
function bech32PolymodStep(pre: number): number {
11+
const b = pre >> 25
12+
return (((pre & 0x1ffffff) << 5) ^
13+
(-((b >> 0) & 1) & 0x3b6a57b2) ^
14+
(-((b >> 1) & 1) & 0x26508e6d) ^
15+
(-((b >> 2) & 1) & 0x1ea119fa) ^
16+
(-((b >> 3) & 1) & 0x3d4233dd) ^
17+
(-((b >> 4) & 1) & 0x2a1462b3))
18+
}
19+
20+
function bech32PrefixChk(prefix: string): number {
21+
let chk = 1
22+
for (let i = 0; i < prefix.length; ++i) {
23+
const c = prefix.charCodeAt(i)
24+
chk = bech32PolymodStep(chk) ^ (c >> 5)
25+
}
26+
chk = bech32PolymodStep(chk)
27+
for (let i = 0; i < prefix.length; ++i) {
28+
chk = bech32PolymodStep(chk) ^ (prefix.charCodeAt(i) & 0x1f)
29+
}
30+
return chk
31+
}
32+
33+
function bech32Convert(data: number[], inBits: number, outBits: number, pad: boolean): number[] {
34+
let value = 0, bits = 0
35+
const maxV = (1 << outBits) - 1
36+
const result: number[] = []
37+
for (const byte of data) {
38+
value = (value << inBits) | byte
39+
bits += inBits
40+
while (bits >= outBits) {
41+
bits -= outBits
42+
result.push((value >> bits) & maxV)
43+
}
44+
}
45+
if (pad && bits > 0) { result.push((value << (outBits - bits)) & maxV) }
46+
return result
47+
}
48+
49+
function bech32Decode(str: string): { prefix: string; words: number[] } {
50+
const lower = str.toLowerCase()
51+
const split = lower.lastIndexOf('1')
52+
if (split < 1 || split + 7 > str.length) { throw new Error(`Invalid bech32: ${str}`) }
53+
const prefix = lower.slice(0, split)
54+
const wordChars = lower.slice(split + 1)
55+
let chk = bech32PrefixChk(prefix)
56+
const words: number[] = []
57+
for (let i = 0; i < wordChars.length; ++i) {
58+
const v = BECH32_ALPHABET_MAP[wordChars[i]]
59+
if (v === undefined) { throw new Error(`Unknown bech32 character: ${wordChars[i]}`) }
60+
chk = bech32PolymodStep(chk) ^ v
61+
if (i + 6 < wordChars.length) { words.push(v) }
62+
}
63+
if (chk !== 1) { throw new Error('Invalid bech32 checksum') }
64+
return { prefix, words }
65+
}
66+
67+
function bech32Encode(prefix: string, words: number[]): string {
68+
prefix = prefix.toLowerCase()
69+
let chk = bech32PrefixChk(prefix)
70+
let result = prefix + '1'
71+
for (const w of words) {
72+
chk = bech32PolymodStep(chk) ^ w
73+
result += BECH32_ALPHABET[w]
74+
}
75+
for (let i = 0; i < 6; ++i) { chk = bech32PolymodStep(chk) }
76+
chk ^= 1
77+
for (let i = 0; i < 6; ++i) { result += BECH32_ALPHABET[(chk >> ((5 - i) * 5)) & 0x1f] }
78+
return result
79+
}
80+
781
export const toJSON = (input: any) => JSON.stringify(input)
882

983
export const toBuffer = (input: any) => Buffer.from(input, 'hex')
@@ -46,18 +120,18 @@ export const fromDBUser = applySpec<User>({
46120
})
47121

48122
export const fromBech32 = (input: string) => {
49-
const { prefix, words } = bech32.decode(input)
123+
const { prefix, words } = bech32Decode(input)
50124
if (!input.startsWith(prefix)) {
51125
throw new Error(`Bech32 invalid prefix: ${prefix}`)
52126
}
53127

54128
return Buffer.from(
55-
bech32.fromWords(words).slice(0, 32)
129+
bech32Convert(words, 5, 8, false).slice(0, 32)
56130
).toString('hex')
57131
}
58132

59133
export const toBech32 = (prefix: string) => (input: string): string => {
60-
return bech32.encode(prefix, bech32.toWords(Buffer.from(input, 'hex')))
134+
return bech32Encode(prefix, bech32Convert(Array.from(Buffer.from(input, 'hex')), 8, 5, true))
61135
}
62136

63137
export const toDate = (input: string | number) => new Date(input)

0 commit comments

Comments
 (0)