feat(utils): refactor settings to use yaml

This commit is contained in:
Anton Livaja 2023-01-11 00:59:18 -05:00 committed by Ricardo Arturo Cabral Mejía
parent 8b8c4b4eaa
commit 5a8107f73c
6 changed files with 272 additions and 379 deletions

View File

@ -354,8 +354,7 @@ You can change the default folder by setting the `NOSTR_CONFIG_DIR` environment
Run nostream using one of the quick-start guides at least once and `nostream/.nostr/settings.json` will be created.
Any changes made to the settings file will be read on the next start.
A sample settings file is included at the project root under the name `settings.sample.json`. Feel free to copy it to `nostream/.nostr/settings.json`
if you would like to have a settings file before running the relay first.
Default settings can be found under `resources/default-settings.yaml`. Feel free to copy it to `nostream/.nostr/settings.yaml` if you would like to have a settings file before running the relay first.
See [CONFIGURATION.md](CONFIGURATION.md) for a detailed explanation of each environment variable and setting.
## Dev Channel

View File

@ -1,216 +0,0 @@
{
"info": {
"relay_url": "wss://nostream.your-domain.com",
"name": "nostream.your-domain.com",
"description": "A nostr relay written in Typescript.",
"pubkey": "replace-with-your-pubkey",
"contact": "operator@your-domain.com"
},
"payments": {
"enabled": false,
"processor": "zebedee",
"feeSchedules": {
"admission": [{
"enabled": false,
"descripton": "Admission fee charged per public key in msats (1000 msats = 1 satoshi)",
"amount": 1000000,
"whitelists": {
"pubkeys": ["replace-with-your-pubkey"]
}
}],
"publication": [
{
"enabled": false,
"description": "Publication fee charged per event in msats (1000 msats = 1 satoshi)",
"amount": 10,
"whitelists": {
"pubkeys": ["replace-with-your-pubkey"]
}
}
]
}
},
"paymentProcessors": {
"zebedee": {
"baseURL": "https://api.zebedee.io/",
"callbackBaseURL": "https://nostream.your-domain.com/callbacks/zebedee"
}
},
"network": {
"maxPayloadSize": 131072,
"remoteIpHeader": "x-forwarded-for",
"idleTimeout": 60
},
"workers": {
"count": 0
},
"limits": {
"invoice": {
"rateLimits": [
{
"period": 60000,
"rate": 1
},
{
"period": 3600000,
"rate": 30
},
{
"period": 86400000,
"rate": 360
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
},
"connection": {
"rateLimits": [
{
"period": 60000,
"rate": 12
},
{
"period": 3600000,
"rate": 360
},
{
"period": 86400000,
"rate": 2880
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
},
"event": {
"eventId": {
"minLeadingZeroBits": 0
},
"kind": {
"whitelist": [],
"blacklist": []
},
"pubkey": {
"minBalanceMsats": 0,
"minLeadingZeroBits": 0,
"whitelist": [],
"blacklist": []
},
"createdAt": {
"maxPositiveDelta": 900,
"maxNegativeDelta": 0
},
"content": [
{
"description": "64 KB for event kind ranges 0-10 and 40-49",
"kinds": [[0, 10], [40, 49]],
"maxLength": 65536
},
{
"description": "96 KB for event kind ranges 11-39 and 50-max",
"kinds": [[11, 39], [50, 9007199254740991]],
"maxLength": 98304
}
],
"rateLimits": [
{
"description": "6 events/min for event kinds 0, 3, 40 and 41",
"kinds": [0, 3, 40, 41],
"period": 60000,
"rate": 6
},
{
"description": "12 events/min for event kinds 1, 2, 4 and 42",
"kinds": [1, 2, 4, 42],
"period": 60000,
"rate": 12
},
{
"description": "360 events/hour for event kinds 1, 2, 4 and 42",
"kinds": [1, 2, 4, 42],
"period": 3600000,
"rate": 360
},
{
"description": "30 events/min for event kind ranges 5-7 and 43-49",
"kinds": [[5, 7], [43, 49]],
"period": 60000,
"rate": 30
},
{
"description": "24 events/min for replaceable events and parameterized replaceable events",
"kinds": [[10000, 19999], [30000, 39999]],
"period": 60000,
"rate": 24
},
{
"description": "60 events/min for ephemeral events",
"kinds": [[20000, 29999]],
"period": 60000,
"rate": 60
},
{
"description": "720 events/hour for all events",
"period": 3600000,
"rate": 720
},
{
"description": "2880 events/day for all events",
"period": 86400000,
"rate": 2880
}
],
"whitelists": {
"pubkeys": [],
"ipAddresses": [
"::1",
"::ffff:10.10.10.1"
]
}
},
"client": {
"subscription": {
"maxSubscriptions": 10,
"maxFilters": 10
}
},
"message": {
"rateLimits": [
{
"description": "60 subscriptions/min",
"types": ["REQ"],
"period": 60000,
"rate": 60
},
{
"description": "2880 subscriptions/hour",
"types": ["REQ"],
"period": 3600000,
"rate": 2880
},
{
"description": "120 raw messages/min",
"period": 60000,
"rate": 120
},
{
"description": "3600 raw messages/hour",
"period": 3600000,
"rate": 3600
},
{
"description": "86400 raw messages/day",
"period": 86400000,
"rate": 86400
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
}
}
}

173
resources/default-settings.yaml Executable file
View File

@ -0,0 +1,173 @@
info:
relay_url: wss://nostream.your-domain.com
name: nostream.your-domain.com
description: A nostr relay written in Typescript.
pubkey: replace-with-your-pubkey
contact: operator@your-domain.com
payments:
enabled: false
processor: zebedee
feeSchedules:
admission:
- enabled: false
descripton: Admission fee charged per public key in msats (1000 msats = 1 satoshi)
amount: 1000000
whitelists:
pubkeys:
- replace-with-your-pubkey
publication:
- enabled: false
description: Publication fee charged per event in msats (1000 msats = 1 satoshi)
amount: 10
whitelists:
pubkeys:
- replace-with-your-pubkey
paymentProcessors:
zebedee:
baseURL: https://api.zebedee.io/
callbackBaseURL: https://nostream.your-domain.com/callbacks/zebedee
network:
maxPayloadSize: 131072
remoteIpHeader: x-forwarded-for
idleTimeout: 60
workers:
count: 0
limits:
invoice:
rateLimits:
- period: 60000
rate: 1
- period: 3600000
rate: 30
- period: 86400000
rate: 360
ipWhitelist:
- "::1"
- "::ffff:10.10.10.1"
connection:
rateLimits:
- period: 60000
rate: 12
- period: 3600000
rate: 360
- period: 86400000
rate: 2880
ipWhitelist:
- "::1"
- "::ffff:10.10.10.1"
event:
eventId:
minLeadingZeroBits: 0
kind:
whitelist: []
blacklist: []
pubkey:
minBalanceMsats: 0
minLeadingZeroBits: 0
whitelist: []
blacklist: []
createdAt:
maxPositiveDelta: 900
maxNegativeDelta: 0
content:
- description: 64 KB for event kind ranges 0-10 and 40-49
kinds:
- - 0
- 10
- - 40
- 49
maxLength: 65536
- description: 96 KB for event kind ranges 11-39 and 50-max
kinds:
- - 11
- 39
- - 50
- 9007199254740991
maxLength: 98304
rateLimits:
- description: 6 events/min for event kinds 0, 3, 40 and 41
kinds:
- 0
- 3
- 40
- 41
period: 60000
rate: 6
- description: 12 events/min for event kinds 1, 2, 4 and 42
kinds:
- 1
- 2
- 4
- 42
period: 60000
rate: 12
- description: 360 events/hour for event kinds 1, 2, 4 and 42
kinds:
- 1
- 2
- 4
- 42
period: 3600000
rate: 360
- description: 30 events/min for event kind ranges 5-7 and 43-49
kinds:
- - 5
- 7
- - 43
- 49
period: 60000
rate: 30
- description: 24 events/min for replaceable events and parameterized replaceable
events
kinds:
- - 10000
- 19999
- - 30000
- 39999
period: 60000
rate: 24
- description: 60 events/min for ephemeral events
kinds:
- - 20000
- 29999
period: 60000
rate: 60
- description: 720 events/hour for all events
period: 3600000
rate: 720
- description: 2880 events/day for all events
period: 86400000
rate: 2880
whitelists:
pubkeys: []
ipAddresses:
- "::1"
- "::ffff:10.10.10.1"
client:
subscription:
maxSubscriptions: 10
maxFilters: 10
message:
rateLimits:
- description: 60 subscriptions/min
types:
- REQ
period: 60000
rate: 60
- description: 2880 subscriptions/hour
types:
- REQ
period: 3600000
rate: 2880
- description: 120 raw messages/min
period: 60000
rate: 120
- description: 3600 raw messages/hour
period: 3600000
rate: 3600
- description: 86400 raw messages/day
period: 86400000
rate: 86400
ipWhitelist:
- "::1"
- "::ffff:10.10.10.1"

View File

@ -1,134 +0,0 @@
{
"info": {
"relay_url": "wss://nostream.your-domain.com",
"name": "nostream.your-domain.com",
"description": "A nostr relay written in TypeScript.",
"pubkey": "replace-with-your-pubkey",
"contact": "operator@your-domain.com"
},
"network": {
"maxPayloadSize": 131072,
"remoteIpHeader": "x-forwarded-for",
"idleTimeout": 60
},
"workers": {
"count": 0
},
"limits": {
"connection": {
"rateLimits": [
{
"period": 60000,
"rate": 12
},
{
"period": 3600000,
"rate": 360
},
{
"period": 86400000,
"rate": 2880
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
},
"event": {
"eventId": {
"minLeadingZeroBits": 0
},
"kind": {
"whitelist": [],
"blacklist": []
},
"pubkey": {
"minBalanceMsats": 10000,
"minLeadingZeroBits": 0,
"whitelist": [],
"blacklist": []
},
"createdAt": {
"maxPositiveDelta": 900,
"maxNegativeDelta": 0
},
"content": {
"maxLength": 1048576
},
"rateLimits": [
{
"kinds": [0, 3, 40, 41],
"period": 60000,
"rate": 6
},
{
"kinds": [1, 2, 4, 42],
"period": 60000,
"rate": 12
},
{
"kinds": [1, 2, 4, 42],
"period": 3600000,
"rate": 360
},
{
"kinds": [[5, 7], [43, 49]],
"period": 60000,
"rate": 30
},
{
"kinds": [[10000, 19999], [30000, 39999]],
"period": 60000,
"rate": 24
},
{
"kinds": [[20000, 29999]],
"period": 60000,
"rate": 60
},
{
"period": 3600000,
"rate": 720
},
{
"period": 86400000,
"rate": 2880
}
],
"whitelists": {
"pubkeys": [],
"ipAddresses": [
"::1",
"::ffff:10.10.10.1"
]
}
},
"client": {
"subscription": {
"maxSubscriptions": 10,
"maxFilters": 10
}
},
"message": {
"rateLimits": [
{
"period": 60000,
"rate": 120
},
{
"period": 3600000,
"rate": 3600
},
{
"period": 86400000,
"rate": 86400
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
}
}
}

View File

@ -2,25 +2,36 @@ import fs from 'fs'
import { homedir } from 'os'
import { join } from 'path'
import { mergeDeepRight } from 'ramda'
import yaml from 'js-yaml'
import { createLogger } from '../factories/logger-factory'
import defaultSettingsJson from '../../resources/default-settings.json'
import { ISettings } from '../@types/settings'
const debug = createLogger('settings')
const FileType = {
yaml: 'yaml',
json: 'json',
}
export class SettingsStatic {
static _settings: ISettings
public static getSettingsFilePath(filename = 'settings.json') {
return join(
process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr'),
filename
)
public static getSettingsFileBasePath(): string {
return process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr')
}
public static loadSettings(path: string) {
debug('loading settings from %s', path)
public static getDefaultSettingsFilePath(): string {
return `${join(process.cwd(), 'resources')}/default-settings.yaml`
}
public static loadAndParseYamlFile(path: string): ISettings {
const defaultSettingsFileContent = fs.readFileSync(path, 'utf8')
const defaults = yaml.load(defaultSettingsFileContent) as ISettings
return defaults
}
public static loadAndParseJsonFile(path: string) {
return JSON.parse(
fs.readFileSync(
path,
@ -29,28 +40,65 @@ export class SettingsStatic {
)
}
public static settingsFileType(path) {
const files = fs.readdirSync(path).filter(fn => fn.startsWith('settings'))
if (files.length) {
const extension = files.pop().split('.')[1]
return FileType[extension]
} else {
return null
}
}
public static loadSettings(path: string, fileType) {
debug('loading settings from %s', path)
switch (fileType) {
case FileType.json: {
debug('settings.json is deprecated, please use a yaml file based on resources/default-settings.yaml')
return this.loadAndParseJsonFile(path)
}
case FileType.yaml: {
return this.loadAndParseYamlFile(path)
}
default: {
throw new Error('settings file was missing or did not contain .yaml or .json extensions.')
}
}
}
public static createSettings(): ISettings {
if (SettingsStatic._settings) {
return SettingsStatic._settings
}
debug('creating settings')
const path = SettingsStatic.getSettingsFilePath()
const defaults = defaultSettingsJson as ISettings
const basePath = SettingsStatic.getSettingsFileBasePath()
if (!fs.existsSync(basePath)) {
fs.mkdirSync(basePath)
}
const defaultsFilePath = SettingsStatic.getDefaultSettingsFilePath()
const fileType = SettingsStatic.settingsFileType(basePath)
const settingsFilePath = `${basePath}/settings.${fileType}`
const defaults = SettingsStatic.loadSettings(defaultsFilePath, FileType.yaml)
try {
if (fs.existsSync(path)) {
if (fileType) {
SettingsStatic._settings = mergeDeepRight(
defaults,
SettingsStatic.loadSettings(path)
SettingsStatic.loadSettings(settingsFilePath, fileType)
)
} else {
SettingsStatic.saveSettings(path, defaults)
SettingsStatic.saveSettings(basePath, defaults)
SettingsStatic._settings = mergeDeepRight({}, defaults)
}
return SettingsStatic._settings
} catch (error) {
debug('error reading config file at %s: %o', path, error)
debug('error reading config file at %s: %o', settingsFilePath, error)
return defaults
}
@ -59,9 +107,9 @@ export class SettingsStatic {
public static saveSettings(path: string, settings: ISettings) {
debug('saving settings to %s: %o', path, settings)
return fs.writeFileSync(
path,
JSON.stringify(settings, null, 2),
{ encoding: 'utf-8' }
`${path}/settings.yaml`,
yaml.dump(settings),
{ encoding: 'utf-8' },
)
}
}

View File

@ -19,22 +19,45 @@ describe('SettingsStatic', () => {
process.env = originalEnv
})
it('returns string ending with settings.json by default', () => {
expect(SettingsStatic.getSettingsFilePath()).to.be.a('string').and.to.match(/settings\.json$/)
})
it('returns string ending with given string', () => {
expect(SettingsStatic.getSettingsFilePath('ending')).to.be.a('string').and.to.match(/ending$/)
it('returns string ending with .nostr/ by default', () => {
expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.to.match(/s\.nostr$/)
})
it('returns path begins with user\'s home dir by default', () => {
expect(SettingsStatic.getSettingsFilePath()).to.be.a('string').and.equal(`${join(homedir(), '.nostr')}/settings.json`)
expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.equal(`${join(homedir(), '.nostr')}/`)
})
it('returns path with NOSTR_CONFIG_DIR if set', () => {
process.env.NOSTR_CONFIG_DIR = '/some/path'
expect(SettingsStatic.getSettingsFilePath()).to.be.a('string').and.equal('/some/path/settings.json')
expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.equal('/some/path/')
})
})
describe('.getDefaultSettingsFilePath', () => {
let originalEnv: NodeJS.ProcessEnv
beforeEach(() => {
originalEnv = process.env
process.env = {}
})
afterEach(() => {
process.env = originalEnv
})
it('returns string ending with settings.json by default', () => {
expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.to.match(/settings\.yaml$/)
})
it('returns path begins with user\'s home dir by default', () => {
expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.equal(`${join(homedir(), '.nostr')}/settings.yaml`)
})
it('returns path with NOSTR_CONFIG_DIR if set', () => {
process.env.NOSTR_CONFIG_DIR = '/some/path'
expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.equal('/some/path/settings.yaml')
})
})
@ -52,7 +75,7 @@ describe('SettingsStatic', () => {
it('loads settings from given path', () => {
readFileSyncStub.returns('"content"')
expect(SettingsStatic.loadSettings('/some/path')).to.equal('content')
expect(SettingsStatic.loadSettings('/some/path', 'yaml')).to.equal('content')
expect(readFileSyncStub).to.have.been.calledOnceWithExactly(
'/some/path',
@ -75,7 +98,7 @@ describe('SettingsStatic', () => {
sandbox = Sinon.createSandbox()
existsSyncStub = sandbox.stub(fs, 'existsSync')
getSettingsFilePathStub = sandbox.stub(SettingsStatic, 'getSettingsFilePath')
getSettingsFilePathStub = sandbox.stub(SettingsStatic, 'getSettingsFileBasePath')
saveSettingsStub = sandbox.stub(SettingsStatic, 'saveSettings')
loadSettingsStub = sandbox.stub(SettingsStatic, 'loadSettings')
})