vestauth 0.18.1 → 0.19.0
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/CHANGELOG.md +13 -1
- package/package.json +1 -1
- package/src/cli/actions/server/start.js +1 -0
- package/src/cli/commands/server.js +1 -4
- package/src/lib/helpers/dbMigrate.js +1 -1
- package/src/lib/helpers/resolvePortAndHostname.js +34 -0
- package/src/lib/helpers/serverStart.js +2 -2
- package/src/lib/helpers/subdomainBaseHost.js +18 -0
- package/src/server/index.js +57 -18
- package/src/server/serializers/rotateSerializer.js +18 -0
- package/src/server/serializers/whoamiSerializer.js +11 -0
- package/src/server/services/rotateService.js +71 -0
- package/src/server/services/whoamiService.js +15 -0
- package/src/db/migrations/.gitkeep +0 -0
- /package/src/{db/migrations → server/db/migration}/20260223204000_create_agents_table.js +0 -0
- /package/src/{db/migrations → server/db/migration}/20260223205500_create_public_jwks_table.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
[Unreleased](https://github.com/vestauth/vestauth/compare/v0.
|
|
5
|
+
[Unreleased](https://github.com/vestauth/vestauth/compare/v0.19.0...main)
|
|
6
|
+
|
|
7
|
+
## [0.19.0](https://github.com/vestauth/vestauth/compare/v0.18.2...v0.19.0) (2026-02-24)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add `POST /rotate` ([#36](https://github.com/vestauth/vestauth/pull/36))
|
|
12
|
+
|
|
13
|
+
## [0.18.2](https://github.com/vestauth/vestauth/compare/v0.18.1...v0.18.2) (2026-02-24)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
* Pass `--hostname` ([#35](https://github.com/vestauth/vestauth/pull/35))
|
|
6
18
|
|
|
7
19
|
## [0.18.1](https://github.com/vestauth/vestauth/compare/v0.18.0...v0.18.1) (2026-02-24)
|
|
8
20
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const { Command } = require('commander')
|
|
2
2
|
const env = require('./../../lib/helpers/env')
|
|
3
3
|
const databaseUrl = require('./../../lib/helpers/databaseUrl')
|
|
4
|
-
const protocol = require('./../../lib/helpers/protocol')
|
|
5
|
-
const hostname = require('./../../lib/helpers/hostname')
|
|
6
4
|
|
|
7
5
|
const server = new Command('server')
|
|
8
6
|
|
|
@@ -15,8 +13,7 @@ const startAction = require('./../actions/server/start')
|
|
|
15
13
|
server.command('start')
|
|
16
14
|
.description('start vestauth server')
|
|
17
15
|
.option('--port <port>', 'port', env('PORT'))
|
|
18
|
-
.option('--
|
|
19
|
-
.option('--hostname <hostname>', 'localhost:3000', hostname())
|
|
16
|
+
.option('--hostname <hostname>', 'HOSTNAME', env('HOSTNAME'))
|
|
20
17
|
.option('--database-url <databaseUrl>', 'DATABASE_URL', databaseUrl())
|
|
21
18
|
.action(startAction)
|
|
22
19
|
|
|
@@ -31,7 +31,7 @@ async function dbMigrate ({ databaseUrl } = {}) {
|
|
|
31
31
|
connection,
|
|
32
32
|
ssl: { rejectUnauthorized: false },
|
|
33
33
|
migrations: {
|
|
34
|
-
directory: path.resolve(__dirname, '../../db/
|
|
34
|
+
directory: path.resolve(__dirname, '../../server/db/migration')
|
|
35
35
|
}
|
|
36
36
|
})
|
|
37
37
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function resolvePortAndHostname ({ port, hostname } = {}) {
|
|
2
|
+
const hasPort = port !== undefined && port !== null && String(port).trim() !== ''
|
|
3
|
+
const inputPort = hasPort ? String(port).trim() : null
|
|
4
|
+
const inputHostname = typeof hostname === 'string' ? hostname.trim() : ''
|
|
5
|
+
|
|
6
|
+
if (!inputHostname) {
|
|
7
|
+
const PORT = inputPort || '3000'
|
|
8
|
+
return {
|
|
9
|
+
PORT,
|
|
10
|
+
HOSTNAME: `http://localhost:${PORT}`
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hasScheme = /^https?:\/\//i.test(inputHostname)
|
|
15
|
+
const bareHostname = hasScheme ? new URL(inputHostname).host : inputHostname
|
|
16
|
+
const bareHostNoPort = bareHostname.split(':')[0].toLowerCase()
|
|
17
|
+
const localHostnames = new Set(['localhost', '127.0.0.1'])
|
|
18
|
+
const defaultScheme = localHostnames.has(bareHostNoPort) ? 'http' : 'https'
|
|
19
|
+
|
|
20
|
+
const url = new URL(hasScheme ? inputHostname : `${defaultScheme}://${inputHostname}`)
|
|
21
|
+
|
|
22
|
+
const PORT = inputPort || url.port || '3000'
|
|
23
|
+
|
|
24
|
+
if (!url.port && localHostnames.has(url.hostname.toLowerCase())) {
|
|
25
|
+
url.port = PORT
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
PORT,
|
|
30
|
+
HOSTNAME: url.toString().replace(/\/$/, '')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = resolvePortAndHostname
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const serverIndex = require('./../../server/index')
|
|
2
2
|
|
|
3
|
-
function serverStart ({ port, databaseUrl }) {
|
|
4
|
-
return serverIndex.start({ port, databaseUrl })
|
|
3
|
+
function serverStart ({ port, hostname, databaseUrl }) {
|
|
4
|
+
return serverIndex.start({ port, hostname, databaseUrl })
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
module.exports = serverStart
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function subdomainBaseHost (hostname) {
|
|
2
|
+
if (!hostname) return null
|
|
3
|
+
|
|
4
|
+
const value = String(hostname).trim().toLowerCase()
|
|
5
|
+
if (!value) return null
|
|
6
|
+
|
|
7
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
8
|
+
try {
|
|
9
|
+
return new URL(value).hostname.toLowerCase()
|
|
10
|
+
} catch {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return value.split('/')[0].split(':')[0]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = subdomainBaseHost
|
package/src/server/index.js
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
const { logger } = require('./../shared/logger')
|
|
2
|
-
const
|
|
2
|
+
const { version } = require('./../lib/helpers/packageJson')
|
|
3
|
+
const resolvePortAndHostname = require('./../lib/helpers/resolvePortAndHostname')
|
|
4
|
+
const subdomainBaseHost = require('./../lib/helpers/subdomainBaseHost')
|
|
3
5
|
const { connectOrm } = require('./models/index')
|
|
4
6
|
const RegisterService = require('./services/registerService')
|
|
5
7
|
const RegisterSerializer = require('./serializers/registerSerializer')
|
|
8
|
+
const RotateService = require('./services/rotateService')
|
|
9
|
+
const RotateSerializer = require('./serializers/rotateSerializer')
|
|
10
|
+
const WhoamiService = require('./services/whoamiService')
|
|
11
|
+
const WhoamiSerializer = require('./serializers/whoamiSerializer')
|
|
6
12
|
|
|
7
13
|
const express = require('express')
|
|
8
14
|
|
|
9
|
-
const app = express()
|
|
10
15
|
let DB = null
|
|
11
16
|
let HTTP_SERVER = null
|
|
12
17
|
let CLOSE_PROMISE = null
|
|
13
18
|
let SIGNAL_HANDLERS_INSTALLED = false
|
|
14
19
|
let SIGNAL_HANDLERS = null
|
|
20
|
+
let PORT = null
|
|
21
|
+
let HOSTNAME = null
|
|
22
|
+
|
|
23
|
+
const app = express()
|
|
15
24
|
app.use(express.json())
|
|
16
25
|
|
|
17
26
|
app.use((req, res, next) => {
|
|
18
27
|
const hostNoPort = (req.headers.host || '').split(':')[0].toLowerCase()
|
|
28
|
+
const baseHost = subdomainBaseHost(HOSTNAME)
|
|
19
29
|
|
|
20
|
-
// agent-c235... .localhost
|
|
21
|
-
if (hostNoPort.endsWith(
|
|
22
|
-
let sub = hostNoPort.slice(0,
|
|
30
|
+
// agent-c235... .localhost or agent-c235... .example.com
|
|
31
|
+
if (baseHost && hostNoPort.endsWith(`.${baseHost}`)) {
|
|
32
|
+
let sub = hostNoPort.slice(0, -`.${baseHost}`.length) // "agent-c235..."
|
|
23
33
|
|
|
24
34
|
// remove "agent-" prefix if present
|
|
25
35
|
if (sub.startsWith('agent-')) {
|
|
@@ -38,7 +48,11 @@ app.get('/', (req, res) => {
|
|
|
38
48
|
if (req.agentUid) {
|
|
39
49
|
res.json({ uid: req.agentUid })
|
|
40
50
|
} else {
|
|
41
|
-
res.json({
|
|
51
|
+
res.json({
|
|
52
|
+
service: 'vestauth',
|
|
53
|
+
status: 'ok',
|
|
54
|
+
version
|
|
55
|
+
})
|
|
42
56
|
}
|
|
43
57
|
})
|
|
44
58
|
|
|
@@ -46,17 +60,14 @@ app.post('/register', async (req, res) => {
|
|
|
46
60
|
try {
|
|
47
61
|
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
|
|
48
62
|
|
|
49
|
-
const {
|
|
50
|
-
agent,
|
|
51
|
-
publicJwk,
|
|
52
|
-
isNew
|
|
53
|
-
} = await new RegisterService({
|
|
63
|
+
const attrs = {
|
|
54
64
|
models: app.models,
|
|
55
65
|
httpMethod: req.method,
|
|
56
66
|
uri: url,
|
|
57
67
|
headers: req.headers,
|
|
58
68
|
publicJwk: req.body.public_jwk
|
|
59
|
-
}
|
|
69
|
+
}
|
|
70
|
+
const { agent, publicJwk, isNew } = await new RegisterService(attrs).run()
|
|
60
71
|
|
|
61
72
|
const json = new RegisterSerializer({ agent, publicJwk, isNew }).run()
|
|
62
73
|
res.json(json)
|
|
@@ -81,17 +92,44 @@ app.get('/.well-known/http-message-signatures-directory', async (req, res) => {
|
|
|
81
92
|
app.get('/whoami', async (req, res) => {
|
|
82
93
|
try {
|
|
83
94
|
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
|
|
84
|
-
const
|
|
95
|
+
const attrs = {
|
|
96
|
+
httpMethod: req.method,
|
|
97
|
+
uri: url,
|
|
98
|
+
headers: req.headers
|
|
99
|
+
}
|
|
100
|
+
const agent = await new WhoamiService(attrs).run()
|
|
101
|
+
|
|
102
|
+
const json = new WhoamiSerializer({ agent }).run()
|
|
103
|
+
res.json(json)
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logger.error(err)
|
|
106
|
+
res.status(401).json({ error: { status: 401, code: 401, message: err.message } })
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
app.post('/rotate', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
|
|
85
113
|
|
|
86
|
-
|
|
114
|
+
const attrs = {
|
|
115
|
+
models: app.models,
|
|
116
|
+
httpMethod: req.method,
|
|
117
|
+
uri: url,
|
|
118
|
+
headers: req.headers,
|
|
119
|
+
publicJwk: req.body.public_jwk
|
|
120
|
+
}
|
|
121
|
+
const { agent, publicJwk } = await new RotateService(attrs).run()
|
|
122
|
+
|
|
123
|
+
const json = new RotateSerializer({ agent, publicJwk }).run()
|
|
124
|
+
res.json(json)
|
|
87
125
|
} catch (err) {
|
|
88
126
|
logger.error(err)
|
|
89
127
|
res.status(401).json({ error: { status: 401, code: 401, message: err.message } })
|
|
90
128
|
}
|
|
91
129
|
})
|
|
92
130
|
|
|
93
|
-
async function start ({ port, databaseUrl } = {}) {
|
|
94
|
-
|
|
131
|
+
async function start ({ port, hostname, databaseUrl } = {}) {
|
|
132
|
+
({ PORT, HOSTNAME } = resolvePortAndHostname({ port, hostname }))
|
|
95
133
|
|
|
96
134
|
if (HTTP_SERVER) return HTTP_SERVER
|
|
97
135
|
|
|
@@ -102,7 +140,7 @@ async function start ({ port, databaseUrl } = {}) {
|
|
|
102
140
|
|
|
103
141
|
HTTP_SERVER = await new Promise((resolve, reject) => {
|
|
104
142
|
const server = app.listen(PORT, () => {
|
|
105
|
-
logger.success(`vestauth server listening on
|
|
143
|
+
logger.success(`vestauth server listening on ${HOSTNAME}`)
|
|
106
144
|
resolve(server)
|
|
107
145
|
})
|
|
108
146
|
|
|
@@ -183,5 +221,6 @@ function removeSignalHandlers () {
|
|
|
183
221
|
module.exports = {
|
|
184
222
|
app,
|
|
185
223
|
start,
|
|
186
|
-
close
|
|
224
|
+
close,
|
|
225
|
+
resolvePortAndHostname
|
|
187
226
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class RotateSerializer {
|
|
2
|
+
constructor ({ agent, publicJwk }) {
|
|
3
|
+
this.agent = agent
|
|
4
|
+
this.publicJwk = publicJwk
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
run () {
|
|
8
|
+
const agentFormatted = this.agent.toJSON()
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
uid: agentFormatted.uidFormatted,
|
|
12
|
+
kid: this.publicJwk.kid,
|
|
13
|
+
public_jwk: this.publicJwk.value
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = RotateSerializer
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const primitives = require('./../../lib/primitives')
|
|
2
|
+
const parseSignatureInputHeader = require('./../../lib/helpers/parseSignatureInputHeader')
|
|
3
|
+
|
|
4
|
+
class RotateService {
|
|
5
|
+
constructor ({ models, httpMethod, uri, headers, publicJwk }) {
|
|
6
|
+
this.models = models
|
|
7
|
+
this.httpMethod = httpMethod
|
|
8
|
+
this.uri = uri
|
|
9
|
+
this.headers = headers
|
|
10
|
+
this.publicJwk = publicJwk
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async run () {
|
|
14
|
+
const signatureInput = this.headers['Signature-Input'] || this.headers['signature-input']
|
|
15
|
+
const signatureInputValues = parseSignatureInputHeader(signatureInput)
|
|
16
|
+
|
|
17
|
+
const kid = signatureInputValues && signatureInputValues.keyid
|
|
18
|
+
if (!kid) throw new Error('kid missing')
|
|
19
|
+
|
|
20
|
+
const newKid = this.publicJwk && this.publicJwk.kid
|
|
21
|
+
if (!newKid) throw new Error('new kid missing')
|
|
22
|
+
|
|
23
|
+
const currentPublicJwk = await this.models.public_jwk.findOne({ kid })
|
|
24
|
+
if (!currentPublicJwk) throw new Error('public_jwk not found')
|
|
25
|
+
|
|
26
|
+
await primitives.verify(this.httpMethod, this.uri, this.headers, currentPublicJwk.value)
|
|
27
|
+
|
|
28
|
+
const agent = await this.models.agent.findOne({ id: currentPublicJwk.agent })
|
|
29
|
+
if (!agent) throw new Error('agent not found')
|
|
30
|
+
|
|
31
|
+
await this.models.public_jwk.db.transaction(async (trx) => {
|
|
32
|
+
const existingNewPublicJwk = await trx('public_jwks')
|
|
33
|
+
.select(['id', 'agent_id', 'kid'])
|
|
34
|
+
.where({ kid: newKid })
|
|
35
|
+
.first()
|
|
36
|
+
|
|
37
|
+
if (existingNewPublicJwk && Number(existingNewPublicJwk.agent_id) !== agent.id) {
|
|
38
|
+
throw new Error('new kid already belongs to another agent')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!existingNewPublicJwk) {
|
|
42
|
+
const now = new Date()
|
|
43
|
+
await trx('public_jwks').insert({
|
|
44
|
+
agent_id: agent.id,
|
|
45
|
+
kid: newKid,
|
|
46
|
+
value: this.publicJwk,
|
|
47
|
+
state: 'active',
|
|
48
|
+
created_at: now,
|
|
49
|
+
updated_at: now
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await trx('public_jwks')
|
|
54
|
+
.where({ id: currentPublicJwk.id })
|
|
55
|
+
.update({
|
|
56
|
+
state: 'revoked',
|
|
57
|
+
updated_at: new Date()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const rotatedPublicJwk = await this.models.public_jwk.findOne({ kid: newKid })
|
|
62
|
+
if (!rotatedPublicJwk) throw new Error('rotated public_jwk not found')
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
agent,
|
|
66
|
+
publicJwk: rotatedPublicJwk
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = RotateService
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const tool = require('./../../lib/tool')
|
|
2
|
+
|
|
3
|
+
class WhoamiService {
|
|
4
|
+
constructor ({ httpMethod, uri, headers }) {
|
|
5
|
+
this.httpMethod = httpMethod
|
|
6
|
+
this.uri = uri
|
|
7
|
+
this.headers = headers
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async run () {
|
|
11
|
+
return tool.verify(this.httpMethod, this.uri, this.headers)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = WhoamiService
|
|
File without changes
|
|
File without changes
|
/package/src/{db/migrations → server/db/migration}/20260223205500_create_public_jwks_table.js
RENAMED
|
File without changes
|