vestauth 0.18.0 → 0.18.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/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.0...main)
5
+ [Unreleased](https://github.com/vestauth/vestauth/compare/v0.18.2...main)
6
+
7
+ ## [0.18.2](https://github.com/vestauth/vestauth/compare/v0.18.1...v0.18.2) (2026-02-24)
8
+
9
+ ### Changed
10
+
11
+ * Pass `--hostname` ([#35](https://github.com/vestauth/vestauth/pull/35))
12
+
13
+ ## [0.18.1](https://github.com/vestauth/vestauth/compare/v0.18.0...v0.18.1) (2026-02-24)
14
+
15
+ ### Changed
16
+
17
+ * Move register logic to service ([#34](https://github.com/vestauth/vestauth/pull/34))
6
18
 
7
19
  ## [0.18.0](https://github.com/vestauth/vestauth/compare/v0.17.0...v0.18.0) (2026-02-24)
8
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vestauth",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "auth for agents–from the creator of dotenvx",
5
5
  "keywords": [
6
6
  "vestauth",
@@ -52,10 +52,8 @@
52
52
  "express": "^4.21.2",
53
53
  "knex": "^3.1.0",
54
54
  "pg": "^8.18.0",
55
- "sails-postgresql": "^5.0.1",
56
55
  "structured-headers": "^2.0.2",
57
- "undici": "7.11.0",
58
- "waterline": "^0.15.2"
56
+ "undici": "7.11.0"
59
57
  },
60
58
  "devDependencies": {
61
59
  "@yao-pkg/pkg": "^5.14.2",
@@ -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,24 +1,31 @@
1
1
  const { logger } = require('./../shared/logger')
2
2
  const tool = require('./../lib/tool')
3
- const primitives = require('./../lib/primitives')
3
+ const resolvePortAndHostname = require('./../lib/helpers/resolvePortAndHostname')
4
+ const subdomainBaseHost = require('./../lib/helpers/subdomainBaseHost')
4
5
  const { connectOrm } = require('./models/index')
6
+ const RegisterService = require('./services/registerService')
7
+ const RegisterSerializer = require('./serializers/registerSerializer')
5
8
 
6
9
  const express = require('express')
7
10
 
8
- const app = express()
9
- let ORM = null
11
+ let DB = null
10
12
  let HTTP_SERVER = null
11
13
  let CLOSE_PROMISE = null
12
14
  let SIGNAL_HANDLERS_INSTALLED = false
13
15
  let SIGNAL_HANDLERS = null
16
+ let PORT = null
17
+ let HOSTNAME = null
18
+
19
+ const app = express()
14
20
  app.use(express.json())
15
21
 
16
22
  app.use((req, res, next) => {
17
23
  const hostNoPort = (req.headers.host || '').split(':')[0].toLowerCase()
24
+ const baseHost = subdomainBaseHost(HOSTNAME)
18
25
 
19
- // agent-c235... .localhost
20
- if (hostNoPort.endsWith('.localhost')) {
21
- let sub = hostNoPort.slice(0, -'.localhost'.length) // "agent-c235..."
26
+ // agent-c235... .localhost or agent-c235... .example.com
27
+ if (baseHost && hostNoPort.endsWith(`.${baseHost}`)) {
28
+ let sub = hostNoPort.slice(0, -`.${baseHost}`.length) // "agent-c235..."
22
29
 
23
30
  // remove "agent-" prefix if present
24
31
  if (sub.startsWith('agent-')) {
@@ -44,24 +51,21 @@ app.get('/', (req, res) => {
44
51
  app.post('/register', async (req, res) => {
45
52
  try {
46
53
  const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
47
- const verified = await primitives.verify(req.method, url, req.headers, req.body.public_jwk)
48
-
49
- const agent = await app.models.agent.create().fetch()
50
54
 
51
- const attrs = {
52
- agent: agent.id,
53
- kid: verified.kid,
54
- value: verified.public_jwk
55
- }
56
- const publicJwk = await app.models.public_jwk.create(attrs).fetch()
57
- const agentFormatted = agent.toJSON()
58
-
59
- res.json({
60
- uid: agentFormatted.uidFormatted,
61
- kid: publicJwk.kid,
62
- public_jwk: verified.public_jwk,
63
- is_new: true
64
- })
55
+ const {
56
+ agent,
57
+ publicJwk,
58
+ isNew
59
+ } = await new RegisterService({
60
+ models: app.models,
61
+ httpMethod: req.method,
62
+ uri: url,
63
+ headers: req.headers,
64
+ publicJwk: req.body.public_jwk
65
+ }).run()
66
+
67
+ const json = new RegisterSerializer({ agent, publicJwk, isNew }).run()
68
+ res.json(json)
65
69
  } catch (err) {
66
70
  logger.error(err)
67
71
  res.status(401).json({ error: { status: 401, code: 401, message: err.message } })
@@ -92,28 +96,19 @@ app.get('/whoami', async (req, res) => {
92
96
  }
93
97
  })
94
98
 
95
- async function start ({ port, databaseUrl } = {}) {
96
- const PORT = port || '3000'
99
+ async function start ({ port, hostname, databaseUrl } = {}) {
100
+ ({ PORT, HOSTNAME } = resolvePortAndHostname({ port, hostname }))
97
101
 
98
102
  if (HTTP_SERVER) return HTTP_SERVER
99
103
 
100
104
  try {
101
- const { orm, config } = connectOrm({ databaseUrl })
102
-
103
- // promisify initialize
104
- const db = await new Promise((resolve, reject) => {
105
- orm.initialize(config, (err, ontology) => {
106
- if (err) return reject(err)
107
- resolve(ontology)
108
- })
109
- })
110
-
111
- ORM = orm
112
- app.models = db.collections
105
+ const { db, models } = connectOrm({ databaseUrl })
106
+ DB = db
107
+ app.models = models
113
108
 
114
109
  HTTP_SERVER = await new Promise((resolve, reject) => {
115
110
  const server = app.listen(PORT, () => {
116
- logger.success(`vestauth server listening on http://localhost:${PORT}`)
111
+ logger.success(`vestauth server listening on ${HOSTNAME}`)
117
112
  resolve(server)
118
113
  })
119
114
 
@@ -145,14 +140,9 @@ async function close () {
145
140
  HTTP_SERVER = null
146
141
  }
147
142
 
148
- if (ORM) {
149
- await new Promise((resolve, reject) => {
150
- ORM.teardown((err) => {
151
- if (err) return reject(err)
152
- resolve()
153
- })
154
- })
155
- ORM = null
143
+ if (DB) {
144
+ await DB.destroy()
145
+ DB = null
156
146
  }
157
147
 
158
148
  delete app.models
@@ -199,5 +189,6 @@ function removeSignalHandlers () {
199
189
  module.exports = {
200
190
  app,
201
191
  start,
202
- close
192
+ close,
193
+ resolvePortAndHostname
203
194
  }
@@ -1,47 +1,72 @@
1
1
  const crypto = require('crypto')
2
- const Waterline = require('waterline')
3
2
 
4
3
  const protocol = require('./../../lib/helpers/protocol')
5
4
  const hostname = require('./../../lib/helpers/hostname')
6
5
 
7
- const Agent = Waterline.Collection.extend({
8
- identity: 'agent',
9
- tableName: 'agents',
10
- datastore: 'default',
11
- primaryKey: 'id',
12
- schema: true,
13
-
14
- attributes: {
15
- id: { type: 'number', autoMigrations: { autoIncrement: true } },
16
- uid: { type: 'string', required: false },
17
- createdAt: { columnName: 'created_at', type: 'ref', autoCreatedAt: true },
18
- updatedAt: { columnName: 'updated_at', type: 'ref', autoUpdatedAt: true },
19
-
20
- // relationships
21
- publicJwks: {
22
- collection: 'public_jwk',
23
- via: 'agent'
24
- }
25
- },
6
+ class AgentRecord {
7
+ constructor (attrs = {}) {
8
+ this.id = attrs.id
9
+ this.uid = attrs.uid
10
+ this.createdAt = attrs.createdAt
11
+ this.updatedAt = attrs.updatedAt
12
+ }
26
13
 
27
- beforeCreate (self, next) {
28
- if (!self.uid) {
29
- const uid = crypto.randomBytes(12).toString('hex')
30
- self.uid = uid
14
+ toJSON () {
15
+ return {
16
+ id: this.id,
17
+ uid: this.uid,
18
+ createdAt: this.createdAt,
19
+ updatedAt: this.updatedAt,
20
+ uidFormatted: `agent-${this.uid}`,
21
+ wellKnownUrl: `${protocol()}://agent-${this.uid}.${hostname()}/.well-known/http-message-signatures-directory`
31
22
  }
32
- next()
33
- },
23
+ }
24
+ }
25
+
26
+ class Agent {
27
+ constructor ({ db }) {
28
+ this.db = db
29
+ this.tableName = 'agents'
30
+ }
31
+
32
+ async create (attrs = {}) {
33
+ const now = new Date()
34
+ const uid = attrs.uid || crypto.randomBytes(12).toString('hex')
35
+
36
+ const [row] = await this.db(this.tableName)
37
+ .insert({
38
+ uid,
39
+ created_at: now,
40
+ updated_at: now
41
+ })
42
+ .returning(['id', 'uid', 'created_at', 'updated_at'])
34
43
 
35
- customToJSON () {
36
- const self = this
44
+ return this._fromRow(row)
45
+ }
46
+
47
+ async findOne (criteria = {}) {
48
+ const query = this.db(this.tableName)
49
+
50
+ if (criteria.id !== undefined) query.where({ id: criteria.id })
51
+ if (criteria.uid !== undefined) query.where({ uid: criteria.uid })
52
+
53
+ const row = await query.select(['id', 'uid', 'created_at', 'updated_at']).first()
54
+
55
+ if (!row) return null
56
+
57
+ return this._fromRow(row)
58
+ }
37
59
 
38
- self.uidFormatted = `agent-${self.uid}`
39
- self.wellKnownUrl = `${protocol()}://${self.uidFormatted}.${hostname()}/.well-known/http-message-signatures-directory`
40
- // remove fields if needed
41
- // delete self.privateKey
60
+ _fromRow (row) {
61
+ if (!row) return null
42
62
 
43
- return self
63
+ return new AgentRecord({
64
+ id: Number(row.id),
65
+ uid: row.uid,
66
+ createdAt: row.created_at,
67
+ updatedAt: row.updated_at
68
+ })
44
69
  }
45
- })
70
+ }
46
71
 
47
72
  module.exports = Agent
@@ -1,31 +1,21 @@
1
- const Waterline = require('waterline')
2
- const sailsPostgresAdapter = require('sails-postgresql')
1
+ const knex = require('knex')
3
2
 
4
3
  const Agent = require('./agent')
5
4
  const PublicJwk = require('./publicJwk')
6
5
 
7
6
  function connectOrm ({ databaseUrl }) {
8
- const orm = new Waterline()
7
+ const db = knex({
8
+ client: 'pg',
9
+ connection: databaseUrl
10
+ })
9
11
 
10
- // register any models
11
- orm.registerModel(Agent)
12
- orm.registerModel(PublicJwk)
13
-
14
- // setup config
15
- const config = {
16
- adapters: {
17
- postgres: sailsPostgresAdapter
18
- },
19
- datastores: {
20
- default: {
21
- adapter: 'postgres',
22
- url: databaseUrl,
23
- migrate: 'safe' // IMPORTANT. instead managed by knex
24
- }
12
+ return {
13
+ db,
14
+ models: {
15
+ agent: new Agent({ db }),
16
+ public_jwk: new PublicJwk({ db })
25
17
  }
26
18
  }
27
-
28
- return { orm, config }
29
19
  }
30
20
 
31
21
  module.exports = { connectOrm }
@@ -1,39 +1,94 @@
1
- const Waterline = require('waterline')
2
-
3
- const PublicJwk = Waterline.Collection.extend({
4
- identity: 'public_jwk',
5
- tableName: 'public_jwks',
6
- datastore: 'default',
7
- primaryKey: 'id',
8
- schema: true,
9
-
10
- attributes: {
11
- id: { type: 'number', autoMigrations: { autoIncrement: true } },
12
- agent: { model: 'agent', columnName: 'agent_id', required: true },
13
- kid: { type: 'string', required: true },
14
- value: { type: 'json', required: true },
15
- state: { type: 'string', required: false },
16
- createdAt: { columnName: 'created_at', type: 'ref', autoCreatedAt: true },
17
- updatedAt: { columnName: 'updated_at', type: 'ref', autoUpdatedAt: true }
18
- },
19
-
20
- beforeCreate (self, next) {
21
- if (!self.state) {
22
- self.state = 'active' // default state
1
+ class PublicJwkRecord {
2
+ constructor (attrs = {}) {
3
+ this.id = attrs.id
4
+ this.agent = attrs.agent
5
+ this.kid = attrs.kid
6
+ this.value = attrs.value
7
+ this.state = attrs.state
8
+ this.createdAt = attrs.createdAt
9
+ this.updatedAt = attrs.updatedAt
10
+ }
11
+
12
+ toJSON () {
13
+ return {
14
+ id: this.id,
15
+ agent: this.agent,
16
+ kid: this.kid,
17
+ value: this.value,
18
+ state: this.state,
19
+ createdAt: this.createdAt,
20
+ updatedAt: this.updatedAt
21
+ }
22
+ }
23
+ }
24
+
25
+ class PublicJwk {
26
+ constructor ({ db }) {
27
+ this.db = db
28
+ this.tableName = 'public_jwks'
29
+ }
30
+
31
+ async create (attrs = {}) {
32
+ const now = new Date()
33
+ const rowToInsert = {
34
+ agent_id: attrs.agent,
35
+ kid: attrs.kid,
36
+ value: attrs.value,
37
+ state: attrs.state || 'active',
38
+ created_at: now,
39
+ updated_at: now
23
40
  }
24
- next()
41
+
42
+ const [row] = await this.db(this.tableName)
43
+ .insert(rowToInsert)
44
+ .returning(['id', 'agent_id', 'kid', 'value', 'state', 'created_at', 'updated_at'])
45
+
46
+ return this._fromRow(row)
47
+ }
48
+
49
+ async find (criteria = {}) {
50
+ const query = this.db(this.tableName)
51
+
52
+ if (criteria.id !== undefined) query.where({ id: criteria.id })
53
+ if (criteria.agent !== undefined) query.where({ agent_id: criteria.agent })
54
+ if (criteria.kid !== undefined) query.where({ kid: criteria.kid })
55
+ if (criteria.state !== undefined) query.where({ state: criteria.state })
56
+
57
+ const rows = await query.select(['id', 'agent_id', 'kid', 'value', 'state', 'created_at', 'updated_at'])
58
+
59
+ return rows.map((row) => this._fromRow(row))
25
60
  }
26
61
 
27
- // customToJSON () {
28
- // const self = this
62
+ async findOne (criteria = {}) {
63
+ const query = this.db(this.tableName)
64
+
65
+ if (criteria.id !== undefined) query.where({ id: criteria.id })
66
+ if (criteria.agent !== undefined) query.where({ agent_id: criteria.agent })
67
+ if (criteria.kid !== undefined) query.where({ kid: criteria.kid })
68
+ if (criteria.state !== undefined) query.where({ state: criteria.state })
29
69
 
30
- // self.uidFormatted = `agent-${self.uid}`
31
- // self.wellKnownUrl = `${protocol()}://${self.uidFormatted}.${hostname()}/.well-known/http-message-signatures-directory`
32
- // // remove fields if needed
33
- // // delete self.privateKey
70
+ const row = await query
71
+ .select(['id', 'agent_id', 'kid', 'value', 'state', 'created_at', 'updated_at'])
72
+ .first()
34
73
 
35
- // return self
36
- // }
37
- })
74
+ if (!row) return null
75
+
76
+ return this._fromRow(row)
77
+ }
78
+
79
+ _fromRow (row) {
80
+ if (!row) return null
81
+
82
+ return new PublicJwkRecord({
83
+ id: Number(row.id),
84
+ agent: Number(row.agent_id),
85
+ kid: row.kid,
86
+ value: row.value,
87
+ state: row.state,
88
+ createdAt: row.created_at,
89
+ updatedAt: row.updated_at
90
+ })
91
+ }
92
+ }
38
93
 
39
94
  module.exports = PublicJwk
@@ -0,0 +1,20 @@
1
+ class RegisterSerializer {
2
+ constructor ({ agent, publicJwk, isNew }) {
3
+ this.agent = agent
4
+ this.publicJwk = publicJwk
5
+ this.isNew = isNew
6
+ }
7
+
8
+ run () {
9
+ const agentFormatted = this.agent.toJSON()
10
+
11
+ return {
12
+ uid: agentFormatted.uidFormatted,
13
+ kid: this.publicJwk.kid,
14
+ public_jwk: this.publicJwk,
15
+ is_new: Boolean(this.isNew)
16
+ }
17
+ }
18
+ }
19
+
20
+ module.exports = RegisterSerializer
@@ -0,0 +1,49 @@
1
+ const primitives = require('./../../lib/primitives')
2
+
3
+ class RegisterService {
4
+ constructor ({ models, httpMethod, uri, headers, publicJwk }) {
5
+ this.models = models
6
+ this.httpMethod = httpMethod
7
+ this.uri = uri
8
+ this.headers = headers
9
+ this.publicJwk = publicJwk
10
+ }
11
+
12
+ async run () {
13
+ this.verified = await primitives.verify(
14
+ this.httpMethod,
15
+ this.uri,
16
+ this.headers,
17
+ this.publicJwk
18
+ )
19
+
20
+ const existingPublicJwk = await this.models.public_jwk.findOne({ kid: this.verified.kid })
21
+ if (existingPublicJwk) {
22
+ const agent = await this.models.agent.findOne({ id: existingPublicJwk.agent })
23
+ if (!agent) {
24
+ throw new Error('agent not found for public_jwk')
25
+ }
26
+
27
+ return {
28
+ agent,
29
+ publicJwk: existingPublicJwk.value,
30
+ isNew: false
31
+ }
32
+ }
33
+
34
+ const agent = await this.models.agent.create()
35
+ const createdPublicJwk = await this.models.public_jwk.create({
36
+ agent: agent.id,
37
+ kid: this.verified.kid,
38
+ value: this.verified.public_jwk
39
+ })
40
+
41
+ return {
42
+ agent,
43
+ publicJwk: createdPublicJwk.value,
44
+ isNew: true
45
+ }
46
+ }
47
+ }
48
+
49
+ module.exports = RegisterService
File without changes
@@ -1,13 +0,0 @@
1
- class Register {
2
- constructor ({ httpMethod, uri, headers, publicJwk }) {
3
- this.httpMethod = httpMethod
4
- this.uri = uri
5
- this.headers = headers
6
- this.publicJwk = publicJwk
7
- }
8
-
9
- async run () {
10
- }
11
- }
12
-
13
- module.exports = Register