Skip to content

Commit 595c0a6

Browse files
Ferryx349cameri
andauthored
feat(admin): extract shared settings-config module for CLI and admin UI (#672)
* feat(admin): extract shared settings-config module for CLI and admin UI Signed-off-by: ABHAY PANDEY <pandeyabhay967@gmail.com> * fix(settings-config): harden path parsing against prototype pollution Signed-off-by: ABHAY PANDEY <pandeyabhay967@gmail.com> * refactor(settings): simplify shared configuration helpers Signed-off-by: ABHAY PANDEY <pandeyabhay967@gmail.com> --------- Signed-off-by: ABHAY PANDEY <pandeyabhay967@gmail.com> Co-authored-by: Ricardo Cabral <me@ricardocabral.io>
1 parent cb7daf6 commit 595c0a6

9 files changed

Lines changed: 740 additions & 567 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
refactor: extract shared settings-config module and guided schema for admin settings editor foundation

src/cli/commands/config.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import yaml from 'js-yaml'
33

44
import {
55
getByPath,
6-
loadDefaults,
76
loadMergedSettings,
87
loadUserSettings,
98
parseTypedValue,
109
saveSettings,
1110
setByPath,
11+
toCategoryLabel,
1212
validatePathAgainstDefaults,
1313
validateSettings,
14-
} from '../utils/config'
14+
} from '../../utils/settings-config'
1515
import {
1616
isSecretEnvKey,
1717
isSupportedEnvKey,
@@ -57,14 +57,6 @@ const serialize = (value: unknown): string => {
5757
return yaml.dump(value, { lineWidth: 120 }).trimEnd()
5858
}
5959

60-
const formatLabel = (key: string): string => {
61-
return key
62-
.split(/[_\-.]/)
63-
.filter(Boolean)
64-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
65-
.join(' ')
66-
}
67-
6860
const restartRelay = async (): Promise<number> => {
6961
const spinner = ora('Restarting relay...').start()
7062

@@ -181,7 +173,6 @@ export const runConfigValidate = async (): Promise<number> => {
181173

182174
return 1
183175
}
184-
185176
export const runConfigEnvList = async (options: { showSecrets?: boolean } = {}): Promise<number> => {
186177
const values = readEnvValues()
187178
const entries = Object.entries(values).sort(([a], [b]) => a.localeCompare(b))
@@ -250,13 +241,8 @@ export const runConfigEnvValidate = async (): Promise<number> => {
250241

251242
logError('Environment validation failed:')
252243
for (const issue of issues) {
253-
logError(`- ${formatLabel(issue.path)} (${issue.path}): ${issue.message}`)
244+
logError(`- ${toCategoryLabel(issue.path)} (${issue.path}): ${issue.message}`)
254245
}
255246

256247
return 1
257248
}
258-
259-
export const getConfigTopLevelCategories = (): string[] => {
260-
const defaults = loadDefaults() as unknown as Record<string, unknown>
261-
return Object.keys(defaults)
262-
}

src/cli/tui/menus/configure.ts

Lines changed: 9 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
import {
2-
getConfigTopLevelCategories,
32
runConfigGet,
43
runConfigList,
54
runConfigSet,
65
runConfigValidate,
76
} from '../../commands/config'
8-
import { getByPath, loadMergedSettings } from '../../utils/config'
7+
import { getByPath, getTopLevelSettingCategories, loadMergedSettings, toCategoryLabel } from '../../../utils/settings-config'
8+
import {
9+
type GuidedSettingField,
10+
guidedSettingCategories,
11+
} from '../../../utils/settings-guided-schema'
912
import { tuiPrompts } from '../prompts'
1013

11-
const toCategoryLabel = (key: string): string => {
12-
return key
13-
.split(/[_\-.]/)
14-
.filter(Boolean)
15-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
16-
.join(' ')
17-
}
18-
1914
const getCategoryOptions = () => {
20-
const categories = getConfigTopLevelCategories().sort((a, b) => a.localeCompare(b))
15+
const categories = getTopLevelSettingCategories().sort((a, b) => a.localeCompare(b))
2116

2217
return [
2318
...categories.map((category) => ({
@@ -28,116 +23,6 @@ const getCategoryOptions = () => {
2823
]
2924
}
3025

31-
type GuidedSetting = {
32-
label: string
33-
path: string
34-
type: 'boolean' | 'number' | 'string' | 'select' | 'stringArray'
35-
options?: string[]
36-
placeholder?: string
37-
validate?: (value: string) => string | undefined
38-
}
39-
40-
type GuidedCategory = {
41-
value: string
42-
label: string
43-
settings: GuidedSetting[]
44-
}
45-
46-
const requireNonEmpty = (value: string): string | undefined => {
47-
return value.trim() ? undefined : 'Value is required'
48-
}
49-
50-
const requireSafeNonNegativeInteger = (value: string): string | undefined => {
51-
const trimmed = value.trim()
52-
if (!/^\d+$/.test(trimmed)) {
53-
return 'Value must be a non-negative integer'
54-
}
55-
56-
const parsed = Number(trimmed)
57-
if (!Number.isSafeInteger(parsed)) {
58-
return 'Value must be a safe integer'
59-
}
60-
61-
return undefined
62-
}
63-
64-
const guidedCategories: GuidedCategory[] = [
65-
{
66-
value: 'payments',
67-
label: 'Payments',
68-
settings: [
69-
{ label: 'Enable payments', path: 'payments.enabled', type: 'boolean' },
70-
{
71-
label: 'Payment processor',
72-
path: 'payments.processor',
73-
type: 'select',
74-
options: ['zebedee', 'lnbits', 'lnurl', 'nodeless', 'opennode'],
75-
},
76-
{
77-
label: 'Admission fee enabled',
78-
path: 'payments.feeSchedules.admission[0].enabled',
79-
type: 'boolean',
80-
},
81-
{
82-
label: 'Admission fee amount (msats)',
83-
path: 'payments.feeSchedules.admission[0].amount',
84-
type: 'number',
85-
validate: requireSafeNonNegativeInteger,
86-
},
87-
],
88-
},
89-
{
90-
value: 'network',
91-
label: 'Network',
92-
settings: [
93-
{
94-
label: 'Relay URL',
95-
path: 'info.relay_url',
96-
type: 'string',
97-
placeholder: 'wss://relay.example.com',
98-
validate: requireNonEmpty,
99-
},
100-
{
101-
label: 'Relay name',
102-
path: 'info.name',
103-
type: 'string',
104-
placeholder: 'relay.example.com',
105-
validate: requireNonEmpty,
106-
},
107-
{
108-
label: 'Max payload size',
109-
path: 'network.maxPayloadSize',
110-
type: 'number',
111-
validate: requireSafeNonNegativeInteger,
112-
},
113-
],
114-
},
115-
{
116-
value: 'limits',
117-
label: 'Limits',
118-
settings: [
119-
{
120-
label: 'Rate limiter strategy',
121-
path: 'limits.rateLimiter.strategy',
122-
type: 'select',
123-
options: ['ewma', 'sliding_window'],
124-
},
125-
{
126-
label: 'Primary event content max length',
127-
path: 'limits.event.content[0].maxLength',
128-
type: 'number',
129-
validate: requireSafeNonNegativeInteger,
130-
},
131-
{
132-
label: 'Minimum pubkey balance',
133-
path: 'limits.event.pubkey.minBalance',
134-
type: 'number',
135-
validate: requireSafeNonNegativeInteger,
136-
},
137-
],
138-
},
139-
]
140-
14126
const formatCurrentValue = (value: unknown): string => {
14227
if (Array.isArray(value)) {
14328
return value.length === 0 ? '[]' : value.join(', ')
@@ -162,7 +47,7 @@ const formatCurrentValue = (value: unknown): string => {
16247
return String(value)
16348
}
16449

165-
const getGuidedSettingValue = async (setting: GuidedSetting, currentValue: unknown) => {
50+
const getGuidedSettingValue = async (setting: GuidedSettingField, currentValue: unknown) => {
16651
switch (setting.type) {
16752
case 'boolean': {
16853
const answer = await tuiPrompts.confirm({
@@ -248,7 +133,7 @@ const getGuidedSettingValue = async (setting: GuidedSetting, currentValue: unkno
248133
const runGuidedConfigureMenu = async (): Promise<number> => {
249134
const category = await tuiPrompts.select({
250135
message: 'Configuration category',
251-
options: [...guidedCategories.map(({ value, label }) => ({ value, label })), { value: 'back', label: 'Back' }],
136+
options: [...guidedSettingCategories.map(({ value, label }) => ({ value, label })), { value: 'back', label: 'Back' }],
252137
})
253138

254139
if (tuiPrompts.isCancel(category)) {
@@ -259,7 +144,7 @@ const runGuidedConfigureMenu = async (): Promise<number> => {
259144
return 0
260145
}
261146

262-
const selectedCategory = guidedCategories.find((entry) => entry.value === category)
147+
const selectedCategory = guidedSettingCategories.find((entry) => entry.value === category)
263148
if (!selectedCategory) {
264149
tuiPrompts.cancel('Unknown category')
265150
return 1

0 commit comments

Comments
 (0)