vestauth 0.16.0 → 0.18.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.16.0...main)
5
+ [Unreleased](https://github.com/vestauth/vestauth/compare/v0.18.0...main)
6
+
7
+ ## [0.18.0](https://github.com/vestauth/vestauth/compare/v0.17.0...v0.18.0) (2026-02-24)
8
+
9
+ ### Added
10
+
11
+ * Add `vestauth server start` for running your own vestauth server ([#33](https://github.com/vestauth/vestauth/pull/33))
12
+
13
+ ## [0.17.0](https://github.com/vestauth/vestauth/compare/v0.16.0...v0.17.0) (2026-02-23)
14
+
15
+ ### Added
16
+
17
+ * Add `db:*` scripts ([#32](https://github.com/vestauth/vestauth/pull/32))
6
18
 
7
19
  ## [0.16.0](https://github.com/vestauth/vestauth/compare/v0.15.1...v0.16.0) (2026-02-23)
8
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vestauth",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "auth for agents–from the creator of dotenvx",
5
5
  "keywords": [
6
6
  "vestauth",
@@ -48,10 +48,14 @@
48
48
  "dependencies": {
49
49
  "@dotenvx/dotenvx": "^1.52.0",
50
50
  "commander": "^11.1.0",
51
- "express": "^4.21.2",
52
51
  "execa": "^5.1.1",
52
+ "express": "^4.21.2",
53
+ "knex": "^3.1.0",
54
+ "pg": "^8.18.0",
55
+ "sails-postgresql": "^5.0.1",
53
56
  "structured-headers": "^2.0.2",
54
- "undici": "7.11.0"
57
+ "undici": "7.11.0",
58
+ "waterline": "^0.15.2"
55
59
  },
56
60
  "devDependencies": {
57
61
  "@yao-pkg/pkg": "^5.14.2",
@@ -0,0 +1,18 @@
1
+ const { logger } = require('./../../../shared/logger')
2
+ const catchAndLog = require('./../../../lib/helpers/catchAndLog')
3
+
4
+ const server = require('./../../../lib/server')
5
+
6
+ async function dbCreate () {
7
+ try {
8
+ const options = this.opts()
9
+ logger.debug(`options: ${JSON.stringify(options)}`)
10
+
11
+ await server.db.create({ databaseUrl: options.databaseUrl })
12
+ } catch (error) {
13
+ catchAndLog(error)
14
+ process.exit(1)
15
+ }
16
+ }
17
+
18
+ module.exports = dbCreate
@@ -0,0 +1,18 @@
1
+ const { logger } = require('./../../../shared/logger')
2
+ const catchAndLog = require('./../../../lib/helpers/catchAndLog')
3
+
4
+ const server = require('./../../../lib/server')
5
+
6
+ async function dbDrop () {
7
+ try {
8
+ const options = this.opts()
9
+ logger.debug(`options: ${JSON.stringify(options)}`)
10
+
11
+ await server.db.drop({ databaseUrl: options.databaseUrl })
12
+ } catch (error) {
13
+ catchAndLog(error)
14
+ process.exit(1)
15
+ }
16
+ }
17
+
18
+ module.exports = dbDrop
@@ -0,0 +1,18 @@
1
+ const { logger } = require('./../../../shared/logger')
2
+ const catchAndLog = require('./../../../lib/helpers/catchAndLog')
3
+
4
+ const server = require('./../../../lib/server')
5
+
6
+ async function dbMigrate () {
7
+ try {
8
+ const options = this.opts()
9
+ logger.debug(`options: ${JSON.stringify(options)}`)
10
+
11
+ await server.db.migrate({ databaseUrl: options.databaseUrl })
12
+ } catch (error) {
13
+ catchAndLog(error)
14
+ process.exit(1)
15
+ }
16
+ }
17
+
18
+ module.exports = dbMigrate
@@ -8,7 +8,10 @@ async function start () {
8
8
  const options = this.opts()
9
9
  logger.debug(`options: ${JSON.stringify(options)}`)
10
10
 
11
- await server.start({ port: options.port })
11
+ await server.start({
12
+ port: options.port,
13
+ databaseUrl: options.databaseUrl
14
+ })
12
15
  } catch (error) {
13
16
  catchAndLog(error)
14
17
  process.exit(1)
@@ -1,5 +1,8 @@
1
1
  const { Command } = require('commander')
2
2
  const env = require('./../../lib/helpers/env')
3
+ const databaseUrl = require('./../../lib/helpers/databaseUrl')
4
+ const protocol = require('./../../lib/helpers/protocol')
5
+ const hostname = require('./../../lib/helpers/hostname')
3
6
 
4
7
  const server = new Command('server')
5
8
 
@@ -12,6 +15,30 @@ const startAction = require('./../actions/server/start')
12
15
  server.command('start')
13
16
  .description('start vestauth server')
14
17
  .option('--port <port>', 'port', env('PORT'))
18
+ .option('--protocol <protocol>', 'https or http', protocol())
19
+ .option('--hostname <hostname>', 'localhost:3000', hostname())
20
+ .option('--database-url <databaseUrl>', 'DATABASE_URL', databaseUrl())
15
21
  .action(startAction)
16
22
 
23
+ // vestauth server db:create
24
+ const dbCreateAction = require('./../actions/server/dbCreate')
25
+ server.command('db:create')
26
+ .description('create vestauth database')
27
+ .option('--database-url <databaseUrl>', 'DATABASE_URL', databaseUrl())
28
+ .action(dbCreateAction)
29
+
30
+ // vestauth server db:migrate
31
+ const dbMigrateAction = require('./../actions/server/dbMigrate')
32
+ server.command('db:migrate')
33
+ .description('run db migrations')
34
+ .option('--database-url <databaseUrl>', 'DATABASE_URL', databaseUrl())
35
+ .action(dbMigrateAction)
36
+
37
+ // vestauth server db:drop
38
+ const dbDropAction = require('./../actions/server/dbDrop')
39
+ server.command('db:drop')
40
+ .description('delete vestauth database')
41
+ .option('--database-url <databaseUrl>', 'DATABASE_URL', databaseUrl())
42
+ .action(dbDropAction)
43
+
17
44
  module.exports = server
File without changes
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @param {import('knex').Knex} knex
3
+ */
4
+ exports.up = async function (knex) {
5
+ await knex.schema.createTable('agents', function (table) {
6
+ table.bigIncrements('id').primary()
7
+ table.string('uid')
8
+ table.datetime('created_at').notNullable()
9
+ table.datetime('updated_at').notNullable()
10
+ table.unique(['uid'], { indexName: 'index_agents_on_uid' })
11
+ })
12
+ }
13
+
14
+ /**
15
+ * @param {import('knex').Knex} knex
16
+ */
17
+ exports.down = async function (knex) {
18
+ await knex.schema.dropTableIfExists('agents')
19
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @param {import('knex').Knex} knex
3
+ */
4
+ exports.up = async function (knex) {
5
+ await knex.schema.createTable('public_jwks', function (table) {
6
+ table.bigIncrements('id').primary()
7
+ table.bigInteger('agent_id').notNullable()
8
+ table.string('kid').notNullable()
9
+ table.jsonb('value').notNullable().defaultTo({})
10
+ table.string('state').notNullable().defaultTo('active')
11
+ table.datetime('created_at').notNullable()
12
+ table.datetime('updated_at').notNullable()
13
+
14
+ table.index(['agent_id'], 'index_public_jwks_on_agent_id')
15
+ table.unique(['kid'], { indexName: 'index_public_jwks_on_kid' })
16
+ table.foreign('agent_id').references('agents.id')
17
+ })
18
+ }
19
+
20
+ /**
21
+ * @param {import('knex').Knex} knex
22
+ */
23
+ exports.down = async function (knex) {
24
+ await knex.schema.dropTableIfExists('public_jwks')
25
+ }
@@ -0,0 +1,7 @@
1
+ const env = require('./env')
2
+
3
+ function databaseUrl () {
4
+ return env('DATABASE_URL') || 'postgres://localhost/vestauth_production'
5
+ }
6
+
7
+ module.exports = databaseUrl
@@ -0,0 +1,57 @@
1
+ const knex = require('knex')
2
+ const { logger } = require('../../shared/logger')
3
+
4
+ function quoteIdentifier (value) {
5
+ return `"${String(value).replace(/"/g, '""')}"`
6
+ }
7
+
8
+ function parseConnectionUrl (value) {
9
+ if (!value) throw new Error('missing DATABASE_URL')
10
+
11
+ let url
12
+ try {
13
+ url = new URL(value)
14
+ } catch {
15
+ throw new Error('invalid DATABASE_URL')
16
+ }
17
+
18
+ const database = decodeURIComponent((url.pathname || '').replace(/^\//, ''))
19
+ if (!database) throw new Error('missing database name in DATABASE_URL')
20
+
21
+ return { url, database }
22
+ }
23
+
24
+ async function dbCreate ({ databaseUrl } = {}) {
25
+ const targetUrl = databaseUrl
26
+ const { url, database } = parseConnectionUrl(targetUrl)
27
+
28
+ // Connect to the maintenance DB (`postgres`) to create the target DB.
29
+ const maintenanceUrl = (() => {
30
+ const copy = new URL(url.toString())
31
+ copy.pathname = '/postgres'
32
+ return copy.toString()
33
+ })()
34
+
35
+ const db = knex({
36
+ client: 'pg',
37
+ connection: maintenanceUrl
38
+ })
39
+
40
+ try {
41
+ const result = await db.raw('select 1 from pg_database where datname = ?', [database])
42
+ const exists = Array.isArray(result.rows) && result.rows.length > 0
43
+
44
+ if (exists) {
45
+ logger.info(`Database '${database}' already exists`)
46
+ return { created: false, database }
47
+ }
48
+
49
+ await db.raw(`create database ${quoteIdentifier(database)}`)
50
+ logger.info(`Created database '${database}'`)
51
+ return { created: true, database }
52
+ } finally {
53
+ await db.destroy()
54
+ }
55
+ }
56
+
57
+ module.exports = dbCreate
@@ -0,0 +1,63 @@
1
+ const knex = require('knex')
2
+ const { logger } = require('../../shared/logger')
3
+
4
+ function quoteIdentifier (value) {
5
+ return `"${String(value).replace(/"/g, '""')}"`
6
+ }
7
+
8
+ function parseConnectionUrl (value) {
9
+ if (!value) throw new Error('missing DATABASE_URL')
10
+
11
+ let url
12
+ try {
13
+ url = new URL(value)
14
+ } catch {
15
+ throw new Error('invalid DATABASE_URL')
16
+ }
17
+
18
+ const database = decodeURIComponent((url.pathname || '').replace(/^\//, ''))
19
+ if (!database) throw new Error('missing database name in DATABASE_URL')
20
+
21
+ return { url, database }
22
+ }
23
+
24
+ async function dbDrop ({ databaseUrl } = {}) {
25
+ const targetUrl = databaseUrl
26
+ const { url, database } = parseConnectionUrl(targetUrl)
27
+
28
+ // Connect to the maintenance DB (`postgres`) to drop the target DB.
29
+ const maintenanceUrl = (() => {
30
+ const copy = new URL(url.toString())
31
+ copy.pathname = '/postgres'
32
+ return copy.toString()
33
+ })()
34
+
35
+ const db = knex({
36
+ client: 'pg',
37
+ connection: maintenanceUrl
38
+ })
39
+
40
+ try {
41
+ const result = await db.raw('select 1 from pg_database where datname = ?', [database])
42
+ const exists = Array.isArray(result.rows) && result.rows.length > 0
43
+
44
+ if (!exists) {
45
+ logger.info(`Database '${database}' does not exist`)
46
+ return { dropped: false, database }
47
+ }
48
+
49
+ // Terminate active connections so DROP DATABASE succeeds in local workflows.
50
+ await db.raw(
51
+ 'select pg_terminate_backend(pid) from pg_stat_activity where datname = ? and pid <> pg_backend_pid()',
52
+ [database]
53
+ )
54
+
55
+ await db.raw(`drop database ${quoteIdentifier(database)}`)
56
+ logger.info(`Dropped database '${database}'`)
57
+ return { dropped: true, database }
58
+ } finally {
59
+ await db.destroy()
60
+ }
61
+ }
62
+
63
+ module.exports = dbDrop
@@ -0,0 +1,93 @@
1
+ const path = require('path')
2
+ const knex = require('knex')
3
+ const Errors = require('./errors')
4
+ const { logger } = require('../../shared/logger')
5
+
6
+ function migrationLabel (filename) {
7
+ const base = String(filename).replace(/\.js$/, '')
8
+ const match = base.match(/^(\d+)_(.+)$/)
9
+
10
+ if (!match) return base
11
+
12
+ const version = match[1]
13
+ const name = match[2]
14
+ .split('_')
15
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
16
+ .join('')
17
+
18
+ return `${version} ${name}`
19
+ }
20
+
21
+ function formatRailsTiming (ms) {
22
+ return `(${(ms / 1000).toFixed(4)}s)`
23
+ }
24
+
25
+ async function dbMigrate ({ databaseUrl } = {}) {
26
+ const connection = databaseUrl
27
+ if (!connection) throw new Errors().missingDatabaseUrl()
28
+
29
+ const db = knex({
30
+ client: 'pg',
31
+ connection,
32
+ ssl: { rejectUnauthorized: false },
33
+ migrations: {
34
+ directory: path.resolve(__dirname, '../../db/migrations')
35
+ }
36
+ })
37
+
38
+ try {
39
+ if (!db.migrate || typeof db.migrate.list !== 'function' || typeof db.migrate.up !== 'function') {
40
+ const startedAt = Date.now()
41
+ const [batchNo, migrations] = await db.migrate.latest()
42
+ const elapsedMs = Date.now() - startedAt
43
+
44
+ for (const migration of migrations) {
45
+ const label = migrationLabel(migration)
46
+ logger.info(`== ${label}: migrating ================================================`)
47
+ logger.info(`== ${label}: migrated ${formatRailsTiming(elapsedMs)} ===========================`)
48
+ }
49
+
50
+ return {
51
+ batchNo,
52
+ migrations
53
+ }
54
+ }
55
+
56
+ const [, pending] = await db.migrate.list()
57
+ const migrations = []
58
+ let batchNo = null
59
+
60
+ for (const pendingMigration of pending) {
61
+ const name = typeof pendingMigration === 'string'
62
+ ? pendingMigration
63
+ : pendingMigration.file || pendingMigration.name
64
+
65
+ const label = migrationLabel(name)
66
+ logger.info(`== ${label}: migrating ================================================`)
67
+
68
+ const startedAt = Date.now()
69
+ const [nextBatchNo, ran] = await db.migrate.up({ name })
70
+ batchNo = nextBatchNo
71
+ const elapsedMs = Date.now() - startedAt
72
+
73
+ logger.info(`== ${label}: migrated ${formatRailsTiming(elapsedMs)} ===========================`)
74
+
75
+ if (Array.isArray(ran)) {
76
+ migrations.push(...ran)
77
+ } else if (ran) {
78
+ migrations.push(ran)
79
+ } else {
80
+ migrations.push(name)
81
+ }
82
+ }
83
+
84
+ return {
85
+ batchNo,
86
+ migrations
87
+ }
88
+ } finally {
89
+ await db.destroy()
90
+ }
91
+ }
92
+
93
+ module.exports = dbMigrate
@@ -121,6 +121,17 @@ class Errors {
121
121
  return e
122
122
  }
123
123
 
124
+ missingDatabaseUrl () {
125
+ const code = 'MISSING_DATABASE_URL'
126
+ const message = `[${code}] missing DATABASE_URL`
127
+ const help = `[${code}] pass --database-url or set DATABASE_URL`
128
+
129
+ const e = new Error(message)
130
+ e.code = code
131
+ e.help = help
132
+ return e
133
+ }
134
+
124
135
  commandFailed () {
125
136
  const code = 'COMMAND_FAILED'
126
137
  const message = `[${code}] command failed with exit code ${this.exitCode}`
@@ -0,0 +1,7 @@
1
+ const env = require('./env')
2
+
3
+ function hostname () {
4
+ return env('HOSTNAME') || 'localhost:3000' // for server
5
+ }
6
+
7
+ module.exports = hostname
@@ -0,0 +1,7 @@
1
+ const env = require('./env')
2
+
3
+ function protocol () {
4
+ return env('PROTOCOL') || 'http'
5
+ }
6
+
7
+ module.exports = protocol
@@ -1,7 +1,7 @@
1
- const serverIndex = require('./../server/index')
1
+ const serverIndex = require('./../../server/index')
2
2
 
3
- function serverStart ({ port }) {
4
- serverIndex.start({ port })
3
+ function serverStart ({ port, databaseUrl }) {
4
+ return serverIndex.start({ port, databaseUrl })
5
5
  }
6
6
 
7
7
  module.exports = serverStart
package/src/lib/server.js CHANGED
@@ -1,5 +1,15 @@
1
1
  const serverStart = require('./helpers/serverStart')
2
+ const dbCreate = require('./helpers/dbCreate')
3
+ const dbMigrate = require('./helpers/dbMigrate')
4
+ const dbDrop = require('./helpers/dbDrop')
5
+ const serverIndex = require('./../server/index')
2
6
 
3
7
  module.exports = {
4
- start: serverStart
8
+ start: serverStart,
9
+ close: serverIndex.close,
10
+ db: {
11
+ create: dbCreate,
12
+ migrate: dbMigrate,
13
+ drop: dbDrop
14
+ }
5
15
  }
@@ -0,0 +1,203 @@
1
+ const { logger } = require('./../shared/logger')
2
+ const tool = require('./../lib/tool')
3
+ const primitives = require('./../lib/primitives')
4
+ const { connectOrm } = require('./models/index')
5
+
6
+ const express = require('express')
7
+
8
+ const app = express()
9
+ let ORM = null
10
+ let HTTP_SERVER = null
11
+ let CLOSE_PROMISE = null
12
+ let SIGNAL_HANDLERS_INSTALLED = false
13
+ let SIGNAL_HANDLERS = null
14
+ app.use(express.json())
15
+
16
+ app.use((req, res, next) => {
17
+ const hostNoPort = (req.headers.host || '').split(':')[0].toLowerCase()
18
+
19
+ // agent-c235... .localhost
20
+ if (hostNoPort.endsWith('.localhost')) {
21
+ let sub = hostNoPort.slice(0, -'.localhost'.length) // "agent-c235..."
22
+
23
+ // remove "agent-" prefix if present
24
+ if (sub.startsWith('agent-')) {
25
+ sub = sub.slice('agent-'.length)
26
+ }
27
+
28
+ req.agentUid = sub
29
+
30
+ return next()
31
+ }
32
+
33
+ next()
34
+ })
35
+
36
+ app.get('/', (req, res) => {
37
+ if (req.agentUid) {
38
+ res.json({ uid: req.agentUid })
39
+ } else {
40
+ res.json({ hello: 'vestauth' })
41
+ }
42
+ })
43
+
44
+ app.post('/register', async (req, res) => {
45
+ try {
46
+ 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
+
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
+ })
65
+ } catch (err) {
66
+ logger.error(err)
67
+ res.status(401).json({ error: { status: 401, code: 401, message: err.message } })
68
+ }
69
+ })
70
+
71
+ app.get('/.well-known/http-message-signatures-directory', async (req, res) => {
72
+ const agent = await app.models.agent.findOne({ uid: req.agentUid })
73
+ if (!agent) {
74
+ return res.status(404).json({ error: { status: 404, code: 404, message: 'not found' } })
75
+ }
76
+
77
+ const jwks = await app.models.public_jwk.find({ agent: agent.id, state: 'active' })
78
+ const keys = jwks.map(j => j.value)
79
+
80
+ return res.json({ keys })
81
+ })
82
+
83
+ app.get('/whoami', async (req, res) => {
84
+ try {
85
+ const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
86
+ const verified = await tool.verify(req.method, url, req.headers)
87
+
88
+ res.json(verified)
89
+ } catch (err) {
90
+ logger.error(err)
91
+ res.status(401).json({ error: { status: 401, code: 401, message: err.message } })
92
+ }
93
+ })
94
+
95
+ async function start ({ port, databaseUrl } = {}) {
96
+ const PORT = port || '3000'
97
+
98
+ if (HTTP_SERVER) return HTTP_SERVER
99
+
100
+ 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
113
+
114
+ HTTP_SERVER = await new Promise((resolve, reject) => {
115
+ const server = app.listen(PORT, () => {
116
+ logger.success(`vestauth server listening on http://localhost:${PORT}`)
117
+ resolve(server)
118
+ })
119
+
120
+ server.once('error', reject)
121
+ })
122
+
123
+ installSignalHandlers()
124
+
125
+ return HTTP_SERVER
126
+ } catch (error) {
127
+ await close().catch(() => {})
128
+ throw error
129
+ }
130
+ }
131
+
132
+ async function close () {
133
+ if (CLOSE_PROMISE) return CLOSE_PROMISE
134
+
135
+ CLOSE_PROMISE = (async () => {
136
+ removeSignalHandlers()
137
+
138
+ if (HTTP_SERVER) {
139
+ await new Promise((resolve, reject) => {
140
+ HTTP_SERVER.close((err) => {
141
+ if (err) return reject(err)
142
+ resolve()
143
+ })
144
+ })
145
+ HTTP_SERVER = null
146
+ }
147
+
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
156
+ }
157
+
158
+ delete app.models
159
+ })()
160
+
161
+ try {
162
+ await CLOSE_PROMISE
163
+ } finally {
164
+ CLOSE_PROMISE = null
165
+ }
166
+ }
167
+
168
+ function installSignalHandlers () {
169
+ if (SIGNAL_HANDLERS_INSTALLED) return
170
+
171
+ const shutdown = () => {
172
+ close()
173
+ .then(() => process.exit(0))
174
+ .catch((err) => {
175
+ logger.error(err)
176
+ process.exit(1)
177
+ })
178
+ }
179
+
180
+ SIGNAL_HANDLERS = {
181
+ SIGINT: () => shutdown('SIGINT'),
182
+ SIGTERM: () => shutdown('SIGTERM')
183
+ }
184
+
185
+ process.once('SIGINT', SIGNAL_HANDLERS.SIGINT)
186
+ process.once('SIGTERM', SIGNAL_HANDLERS.SIGTERM)
187
+ SIGNAL_HANDLERS_INSTALLED = true
188
+ }
189
+
190
+ function removeSignalHandlers () {
191
+ if (!SIGNAL_HANDLERS_INSTALLED || !SIGNAL_HANDLERS) return
192
+
193
+ process.removeListener('SIGINT', SIGNAL_HANDLERS.SIGINT)
194
+ process.removeListener('SIGTERM', SIGNAL_HANDLERS.SIGTERM)
195
+ SIGNAL_HANDLERS = null
196
+ SIGNAL_HANDLERS_INSTALLED = false
197
+ }
198
+
199
+ module.exports = {
200
+ app,
201
+ start,
202
+ close
203
+ }
@@ -0,0 +1,47 @@
1
+ const crypto = require('crypto')
2
+ const Waterline = require('waterline')
3
+
4
+ const protocol = require('./../../lib/helpers/protocol')
5
+ const hostname = require('./../../lib/helpers/hostname')
6
+
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
+ },
26
+
27
+ beforeCreate (self, next) {
28
+ if (!self.uid) {
29
+ const uid = crypto.randomBytes(12).toString('hex')
30
+ self.uid = uid
31
+ }
32
+ next()
33
+ },
34
+
35
+ customToJSON () {
36
+ const self = this
37
+
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
42
+
43
+ return self
44
+ }
45
+ })
46
+
47
+ module.exports = Agent
@@ -0,0 +1,31 @@
1
+ const Waterline = require('waterline')
2
+ const sailsPostgresAdapter = require('sails-postgresql')
3
+
4
+ const Agent = require('./agent')
5
+ const PublicJwk = require('./publicJwk')
6
+
7
+ function connectOrm ({ databaseUrl }) {
8
+ const orm = new Waterline()
9
+
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
+ }
25
+ }
26
+ }
27
+
28
+ return { orm, config }
29
+ }
30
+
31
+ module.exports = { connectOrm }
@@ -0,0 +1,39 @@
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
23
+ }
24
+ next()
25
+ }
26
+
27
+ // customToJSON () {
28
+ // const self = this
29
+
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
34
+
35
+ // return self
36
+ // }
37
+ })
38
+
39
+ module.exports = PublicJwk
@@ -0,0 +1,13 @@
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
@@ -1,99 +0,0 @@
1
- const { logger } = require('./../../shared/logger')
2
- const tool = require('./../tool')
3
- const primitives = require('./../primitives')
4
-
5
- const express = require('express')
6
- const crypto = require('crypto')
7
-
8
- const AGENTS = []
9
- const PUBLIC_JWKS = []
10
-
11
- const app = express()
12
- app.use(express.json())
13
-
14
- app.use((req, res, next) => {
15
- const hostNoPort = (req.headers.host || '').split(':')[0].toLowerCase()
16
-
17
- // agent-c235... .localhost
18
- if (hostNoPort.endsWith('.localhost')) {
19
- const sub = hostNoPort.slice(0, -'.localhost'.length) // "agent-c235..."
20
- req.agentUid = sub
21
- return next()
22
- }
23
-
24
- next()
25
- })
26
-
27
- app.get('/', (req, res) => {
28
- if (req.agentUid) {
29
- res.json({ uid: req.agentUid })
30
- } else {
31
- res.json({ hello: 'vestauth' })
32
- }
33
- })
34
-
35
- app.post('/register', async (req, res) => {
36
- try {
37
- const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
38
- const verified = await primitives.verify(req.method, url, req.headers, req.body.public_jwk)
39
-
40
- // insert agent
41
- const uid = `agent-${crypto.randomBytes(12).toString('hex')}`
42
- const agent = { uid }
43
- AGENTS.push(agent)
44
-
45
- // insert public_jwk
46
- const publicJwk = {
47
- agent_uid: agent.uid,
48
- kid: verified.kid,
49
- value: verified.public_jwk
50
- }
51
- PUBLIC_JWKS.push(publicJwk)
52
-
53
- // response must be this format
54
- const json = {
55
- uid: agent.uid,
56
- kid: publicJwk.kid,
57
- public_jwk: verified.public_jwk,
58
- is_new: true
59
- }
60
-
61
- res.json(json)
62
- } catch (err) {
63
- logger.error(err)
64
- res.status(401).json({ error: { status: 401, code: 401, message: err.message } })
65
- }
66
- })
67
-
68
- app.get('/.well-known/http-message-signatures-directory', (req, res) => {
69
- const keys = PUBLIC_JWKS
70
- .filter(jwk => jwk.agent_uid === req.agentUid)
71
- .map(jwk => jwk.value)
72
-
73
- res.json({ keys })
74
- })
75
-
76
- app.get('/whoami', async (req, res) => {
77
- try {
78
- const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
79
- const verified = await tool.verify(req.method, url, req.headers)
80
-
81
- res.json(verified)
82
- } catch (err) {
83
- logger.error(err)
84
- res.status(401).json({ error: { status: 401, code: 401, message: err.message } })
85
- }
86
- })
87
-
88
- function start ({ port } = {}) {
89
- const PORT = port || '3000'
90
-
91
- return app.listen(PORT, () => {
92
- logger.success(`vestauth server listening on http://localhost:${PORT}`)
93
- })
94
- }
95
-
96
- module.exports = {
97
- app,
98
- start
99
- }