@defra/lis-infra-ui-services 0.40.0 → 0.41.0-lreg-506.alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/package.json +3 -1
- package/src/config-formats.js +56 -0
- package/src/config-formats.test.js +158 -0
package/README.md
CHANGED
|
@@ -95,6 +95,33 @@ const serveStaticFiles = createStaticFilesPlugin({
|
|
|
95
95
|
It serves `/favicon.ico` and the files under each asset path. `/status-codes` exports `statusCodes`.
|
|
96
96
|
`/duration` exports the `milliseconds` and `seconds` lookups and `unixEpoch()`.
|
|
97
97
|
|
|
98
|
+
### Config formats
|
|
99
|
+
|
|
100
|
+
`/config-formats` exports convict formats for settings the hubs and spokes share. Register each with `convict.addFormat()`; a bad value fails `config.validate()` with the setting named.
|
|
101
|
+
|
|
102
|
+
- `positiveInteger` (`'positive-integer'`): a whole number of at least 1, for timeouts, TTLs and circuit breaker thresholds, where 0 is a misconfiguration rather than "off". Values from environment variables are coerced from strings.
|
|
103
|
+
- `secret` (`'secret'`): a signing or encryption secret of at least 32 characters with no default, for `HUB_JWT_SECRET` and `SESSION_COOKIE_PASSWORD`. The setting must also be marked `sensitive: true`, or convict appends the value to the validation error.
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import convict from 'convict'
|
|
107
|
+
import { positiveInteger, secret } from '@defra/lis-infra-ui-services/config-formats'
|
|
108
|
+
|
|
109
|
+
convict.addFormat(positiveInteger)
|
|
110
|
+
convict.addFormat(secret)
|
|
111
|
+
|
|
112
|
+
timeout: {
|
|
113
|
+
format: 'positive-integer',
|
|
114
|
+
default: 5000,
|
|
115
|
+
env: 'CATTLE_HOME_API_TIMEOUT'
|
|
116
|
+
},
|
|
117
|
+
hubJwtSecret: {
|
|
118
|
+
format: 'secret',
|
|
119
|
+
default: null,
|
|
120
|
+
env: 'HUB_JWT_SECRET',
|
|
121
|
+
sensitive: true
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
98
125
|
### Security headers
|
|
99
126
|
|
|
100
127
|
`securityHeaders` is a hapi plugin that adds HSTS, `x-content-type-options`, `referrer-policy` and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/lis-infra-ui-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0-lreg-506.alpha.2",
|
|
4
4
|
"description": "UI-Services shared definitions for the livestock taxonomy topology.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.js",
|
|
12
12
|
"./base-path": "./src/base-path.js",
|
|
13
|
+
"./config-formats": "./src/config-formats.js",
|
|
13
14
|
"./cookies": "./src/cookies/index.js",
|
|
14
15
|
"./cookies/cookies-policy": "./src/cookies/cookies-policy.js",
|
|
15
16
|
"./components/autocomplete": "./src/components/autocomplete.js",
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
"@hapi/hapi": "^21.4.10",
|
|
88
89
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
89
90
|
"audit-ci": "^7.1.0",
|
|
91
|
+
"convict": "^6.2.5",
|
|
90
92
|
"eslint": "^9.0.0",
|
|
91
93
|
"govuk-frontend": "6.5.0",
|
|
92
94
|
"husky": "^9.1.7",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A convict format for settings that must be a whole number of at least 1,
|
|
3
|
+
* such as timeouts and circuit breaker thresholds, where 0 is a
|
|
4
|
+
* misconfiguration rather than "off". Register it with
|
|
5
|
+
* `convict.addFormat(positiveInteger)` and use `format: 'positive-integer'`.
|
|
6
|
+
*/
|
|
7
|
+
export const positiveInteger = {
|
|
8
|
+
name: 'positive-integer',
|
|
9
|
+
/**
|
|
10
|
+
* @param {unknown} value the raw value, a string when it came from an environment variable
|
|
11
|
+
* @returns {unknown} the value as a number when it is numeric, otherwise unchanged so validate() can reject it
|
|
12
|
+
*/
|
|
13
|
+
coerce(value) {
|
|
14
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
|
|
15
|
+
return Number(value)
|
|
16
|
+
}
|
|
17
|
+
return value
|
|
18
|
+
},
|
|
19
|
+
/**
|
|
20
|
+
* @param {unknown} value the coerced value
|
|
21
|
+
*/
|
|
22
|
+
validate(value) {
|
|
23
|
+
if (
|
|
24
|
+
typeof value !== 'number' ||
|
|
25
|
+
!Number.isSafeInteger(value) ||
|
|
26
|
+
value < 1
|
|
27
|
+
) {
|
|
28
|
+
throw new Error('must be a positive integer')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MINIMUM_SECRET_LENGTH = 32
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A convict format for a signing or encryption secret: a string of at least
|
|
37
|
+
* 32 characters, with no default. The format's own error names the setting,
|
|
38
|
+
* never the value, but convict appends the value to a validation error unless
|
|
39
|
+
* the setting is marked `sensitive: true`. Register it with
|
|
40
|
+
* `convict.addFormat(secret)` and use `format: 'secret'` with
|
|
41
|
+
* `default: null` and `sensitive: true`.
|
|
42
|
+
*/
|
|
43
|
+
export const secret = {
|
|
44
|
+
name: 'secret',
|
|
45
|
+
/**
|
|
46
|
+
* @param {unknown} value
|
|
47
|
+
*/
|
|
48
|
+
validate(value) {
|
|
49
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
50
|
+
throw new Error('must be set')
|
|
51
|
+
}
|
|
52
|
+
if (value.length < MINIMUM_SECRET_LENGTH) {
|
|
53
|
+
throw new Error(`must be at least ${MINIMUM_SECRET_LENGTH} characters`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import convict from 'convict'
|
|
3
|
+
|
|
4
|
+
import { positiveInteger, secret } from './config-formats.js'
|
|
5
|
+
|
|
6
|
+
convict.addFormat(positiveInteger)
|
|
7
|
+
convict.addFormat(secret)
|
|
8
|
+
|
|
9
|
+
function createConfig(value) {
|
|
10
|
+
return convict({
|
|
11
|
+
timeout: {
|
|
12
|
+
format: 'positive-integer',
|
|
13
|
+
default: 5000,
|
|
14
|
+
env: 'TEST_TIMEOUT'
|
|
15
|
+
}
|
|
16
|
+
}).load({ timeout: value })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('positiveInteger', () => {
|
|
20
|
+
test('is registered as the positive-integer format', () => {
|
|
21
|
+
// Assert
|
|
22
|
+
expect(positiveInteger.name).toBe('positive-integer')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test.each([1, 30000])('accepts %s', (value) => {
|
|
26
|
+
// Act
|
|
27
|
+
let error
|
|
28
|
+
try {
|
|
29
|
+
createConfig(value).validate({ allowed: 'strict' })
|
|
30
|
+
} catch (e) {
|
|
31
|
+
error = e
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(error).toBeUndefined()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('coerces a numeric string from an environment variable', () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
process.env.TEST_TIMEOUT = '2500'
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
let config, error
|
|
44
|
+
try {
|
|
45
|
+
config = convict({
|
|
46
|
+
timeout: {
|
|
47
|
+
format: 'positive-integer',
|
|
48
|
+
default: 5000,
|
|
49
|
+
env: 'TEST_TIMEOUT'
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
config.validate({ allowed: 'strict' })
|
|
53
|
+
} catch (e) {
|
|
54
|
+
error = e
|
|
55
|
+
} finally {
|
|
56
|
+
delete process.env.TEST_TIMEOUT
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(error).toBeUndefined()
|
|
61
|
+
expect(config.get('timeout')).toBe(2500)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test.each([0, -1, 1.5, '0', 'abc', '', null, true, '99999999999999999999'])(
|
|
65
|
+
'rejects %j naming the setting',
|
|
66
|
+
(value) => {
|
|
67
|
+
// Act
|
|
68
|
+
let error
|
|
69
|
+
try {
|
|
70
|
+
createConfig(value).validate({ allowed: 'strict' })
|
|
71
|
+
} catch (e) {
|
|
72
|
+
error = e
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
expect(error.message).toContain('timeout: must be a positive integer')
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('secret', () => {
|
|
82
|
+
function createSecretConfig(value) {
|
|
83
|
+
return convict({
|
|
84
|
+
password: {
|
|
85
|
+
format: 'secret',
|
|
86
|
+
default: null,
|
|
87
|
+
sensitive: true
|
|
88
|
+
}
|
|
89
|
+
}).load({ password: value })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test('is registered as the secret format', () => {
|
|
93
|
+
// Assert
|
|
94
|
+
expect(secret.name).toBe('secret')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('accepts a string of at least 32 characters', () => {
|
|
98
|
+
// Act
|
|
99
|
+
let error
|
|
100
|
+
try {
|
|
101
|
+
createSecretConfig('a'.repeat(32)).validate({ allowed: 'strict' })
|
|
102
|
+
} catch (e) {
|
|
103
|
+
error = e
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
107
|
+
expect(error).toBeUndefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test.each([null, '', 42])('rejects %j as unset', (value) => {
|
|
111
|
+
// Act
|
|
112
|
+
let error
|
|
113
|
+
try {
|
|
114
|
+
createSecretConfig(value).validate({ allowed: 'strict' })
|
|
115
|
+
} catch (e) {
|
|
116
|
+
error = e
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
expect(error.message).toContain('password: must be set')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('rejects a short secret without echoing it', () => {
|
|
124
|
+
// Arrange
|
|
125
|
+
const value = 'too-short-secret'
|
|
126
|
+
|
|
127
|
+
// Act
|
|
128
|
+
let error
|
|
129
|
+
try {
|
|
130
|
+
createSecretConfig(value).validate({ allowed: 'strict' })
|
|
131
|
+
} catch (e) {
|
|
132
|
+
error = e
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Assert
|
|
136
|
+
expect(error.message).toContain('password: must be at least 32 characters')
|
|
137
|
+
expect(error.message).not.toContain(value)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('relies on sensitive: true to keep the value out of the error', () => {
|
|
141
|
+
// Arrange
|
|
142
|
+
const value = 'too-short-secret'
|
|
143
|
+
const config = convict({
|
|
144
|
+
password: { format: 'secret', default: null }
|
|
145
|
+
}).load({ password: value })
|
|
146
|
+
|
|
147
|
+
// Act
|
|
148
|
+
let error
|
|
149
|
+
try {
|
|
150
|
+
config.validate({ allowed: 'strict' })
|
|
151
|
+
} catch (e) {
|
|
152
|
+
error = e
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Assert
|
|
156
|
+
expect(error.message).toContain(value)
|
|
157
|
+
})
|
|
158
|
+
})
|