vestauth 0.17.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 +7 -1
- package/package.json +4 -2
- package/src/cli/commands/server.js +4 -0
- package/src/lib/helpers/dbCreate.js +3 -0
- package/src/lib/helpers/dbDrop.js +3 -0
- package/src/lib/helpers/dbMigrate.js +64 -1
- package/src/lib/helpers/hostname.js +7 -0
- package/src/lib/helpers/protocol.js +7 -0
- package/src/lib/helpers/serverStart.js +1 -1
- package/src/lib/server.js +2 -0
- package/src/server/index.js +203 -0
- package/src/server/models/agent.js +47 -0
- package/src/server/models/index.js +31 -0
- package/src/server/models/publicJwk.js +39 -0
- package/src/server/services/register.js +13 -0
- package/src/lib/server/index.js +0 -99
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
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.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))
|
|
6
12
|
|
|
7
13
|
## [0.17.0](https://github.com/vestauth/vestauth/compare/v0.16.0...v0.17.0) (2026-02-23)
|
|
8
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vestauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "auth for agents–from the creator of dotenvx",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vestauth",
|
|
@@ -52,8 +52,10 @@
|
|
|
52
52
|
"express": "^4.21.2",
|
|
53
53
|
"knex": "^3.1.0",
|
|
54
54
|
"pg": "^8.18.0",
|
|
55
|
+
"sails-postgresql": "^5.0.1",
|
|
55
56
|
"structured-headers": "^2.0.2",
|
|
56
|
-
"undici": "7.11.0"
|
|
57
|
+
"undici": "7.11.0",
|
|
58
|
+
"waterline": "^0.15.2"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"@yao-pkg/pkg": "^5.14.2",
|
|
@@ -1,6 +1,8 @@
|
|
|
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')
|
|
4
6
|
|
|
5
7
|
const server = new Command('server')
|
|
6
8
|
|
|
@@ -13,6 +15,8 @@ const startAction = require('./../actions/server/start')
|
|
|
13
15
|
server.command('start')
|
|
14
16
|
.description('start vestauth server')
|
|
15
17
|
.option('--port <port>', 'port', env('PORT'))
|
|
18
|
+
.option('--protocol <protocol>', 'https or http', protocol())
|
|
19
|
+
.option('--hostname <hostname>', 'localhost:3000', hostname())
|
|
16
20
|
.option('--database-url <databaseUrl>', 'DATABASE_URL', databaseUrl())
|
|
17
21
|
.action(startAction)
|
|
18
22
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const knex = require('knex')
|
|
2
|
+
const { logger } = require('../../shared/logger')
|
|
2
3
|
|
|
3
4
|
function quoteIdentifier (value) {
|
|
4
5
|
return `"${String(value).replace(/"/g, '""')}"`
|
|
@@ -41,10 +42,12 @@ async function dbCreate ({ databaseUrl } = {}) {
|
|
|
41
42
|
const exists = Array.isArray(result.rows) && result.rows.length > 0
|
|
42
43
|
|
|
43
44
|
if (exists) {
|
|
45
|
+
logger.info(`Database '${database}' already exists`)
|
|
44
46
|
return { created: false, database }
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
await db.raw(`create database ${quoteIdentifier(database)}`)
|
|
50
|
+
logger.info(`Created database '${database}'`)
|
|
48
51
|
return { created: true, database }
|
|
49
52
|
} finally {
|
|
50
53
|
await db.destroy()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const knex = require('knex')
|
|
2
|
+
const { logger } = require('../../shared/logger')
|
|
2
3
|
|
|
3
4
|
function quoteIdentifier (value) {
|
|
4
5
|
return `"${String(value).replace(/"/g, '""')}"`
|
|
@@ -41,6 +42,7 @@ async function dbDrop ({ databaseUrl } = {}) {
|
|
|
41
42
|
const exists = Array.isArray(result.rows) && result.rows.length > 0
|
|
42
43
|
|
|
43
44
|
if (!exists) {
|
|
45
|
+
logger.info(`Database '${database}' does not exist`)
|
|
44
46
|
return { dropped: false, database }
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -51,6 +53,7 @@ async function dbDrop ({ databaseUrl } = {}) {
|
|
|
51
53
|
)
|
|
52
54
|
|
|
53
55
|
await db.raw(`drop database ${quoteIdentifier(database)}`)
|
|
56
|
+
logger.info(`Dropped database '${database}'`)
|
|
54
57
|
return { dropped: true, database }
|
|
55
58
|
} finally {
|
|
56
59
|
await db.destroy()
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
2
|
const knex = require('knex')
|
|
3
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
|
+
}
|
|
4
24
|
|
|
5
25
|
async function dbMigrate ({ databaseUrl } = {}) {
|
|
6
26
|
const connection = databaseUrl
|
|
@@ -16,7 +36,50 @@ async function dbMigrate ({ databaseUrl } = {}) {
|
|
|
16
36
|
})
|
|
17
37
|
|
|
18
38
|
try {
|
|
19
|
-
|
|
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
|
+
}
|
|
20
83
|
|
|
21
84
|
return {
|
|
22
85
|
batchNo,
|
package/src/lib/server.js
CHANGED
|
@@ -2,9 +2,11 @@ const serverStart = require('./helpers/serverStart')
|
|
|
2
2
|
const dbCreate = require('./helpers/dbCreate')
|
|
3
3
|
const dbMigrate = require('./helpers/dbMigrate')
|
|
4
4
|
const dbDrop = require('./helpers/dbDrop')
|
|
5
|
+
const serverIndex = require('./../server/index')
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
start: serverStart,
|
|
9
|
+
close: serverIndex.close,
|
|
8
10
|
db: {
|
|
9
11
|
create: dbCreate,
|
|
10
12
|
migrate: dbMigrate,
|
|
@@ -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
|
package/src/lib/server/index.js
DELETED
|
@@ -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, databaseUrl: _databaseUrl } = {}) {
|
|
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
|
-
}
|