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 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.18.1...main)
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,6 +1,6 @@
1
1
  {
2
2
  "name": "vestauth",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "auth for agents–from the creator of dotenvx",
5
5
  "keywords": [
6
6
  "vestauth",
@@ -10,6 +10,7 @@ async function start () {
10
10
 
11
11
  await server.start({
12
12
  port: options.port,
13
+ hostname: options.hostname,
13
14
  databaseUrl: options.databaseUrl
14
15
  })
15
16
  } catch (error) {
@@ -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('--protocol <protocol>', 'https or http', protocol())
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/migrations')
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
@@ -1,25 +1,35 @@
1
1
  const { logger } = require('./../shared/logger')
2
- const tool = require('./../lib/tool')
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('.localhost')) {
22
- let sub = hostNoPort.slice(0, -'.localhost'.length) // "agent-c235..."
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({ hello: 'vestauth' })
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
- }).run()
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 verified = await tool.verify(req.method, url, req.headers)
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
- res.json(verified)
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
- const PORT = port || '3000'
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 http://localhost:${PORT}`)
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,11 @@
1
+ class WhoamiSerializer {
2
+ constructor ({ agent }) {
3
+ this.agent = agent
4
+ }
5
+
6
+ run () {
7
+ return this.agent
8
+ }
9
+ }
10
+
11
+ module.exports = WhoamiSerializer
@@ -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