vestauth 0.14.1 → 0.15.1

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,20 @@
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.14.1...main)
5
+ [Unreleased](https://github.com/vestauth/vestauth/compare/v0.15.1...main)
6
+
7
+ ## [0.15.1](https://github.com/vestauth/vestauth/compare/v0.15.0...v0.15.1) (2026-02-23)
8
+
9
+ ### Added
10
+
11
+ * Add better error handling for `verify` missing headers ([#25](https://github.com/vestauth/vestauth/pull/25))
12
+
13
+ ## [0.15.0](https://github.com/vestauth/vestauth/compare/v0.14.1...v0.15.0) (2026-02-20)
14
+
15
+ ### Added
16
+
17
+ * Add support for http localhost ([#23](https://github.com/vestauth/vestauth/pull/23))
18
+ * Add support for localhost public key discovery ([#24](https://github.com/vestauth/vestauth/pull/24))
6
19
 
7
20
  ## [0.14.1](https://github.com/vestauth/vestauth/compare/v0.14.0...v0.14.1) (2026-02-18)
8
21
 
package/README.md CHANGED
@@ -212,6 +212,7 @@ $ vestauth agent init
212
212
  <details><summary>`agent init --hostname`</summary><br>
213
213
 
214
214
  Use `--hostname` to override the agent API hostname (defaults to `AGENT_HOSTNAME`, then `api.vestauth.com`):
215
+ When no scheme is provided, `https://` is assumed. For local non-TLS endpoints, pass `http://...` explicitly.
215
216
 
216
217
  ```sh
217
218
  $ vestauth agent init --hostname https://vestauth.yoursite.com
@@ -372,7 +373,16 @@ Use vestauth directly in code.
372
373
  Verify and authenticate an agent's cryptographic identity.
373
374
 
374
375
  ```js
375
- const agent = await vestauth.tool.verify(req.method, url, req.headers)
376
+ const agent = await vestauth.tool.verify(httpMethod, url, headers)
377
+ ```
378
+
379
+ </details>
380
+ <details><summary>`primitives.verify()`</summary><br>
381
+
382
+ Verify and authenticate a signed http request.
383
+
384
+ ```js
385
+ await vestauth.primitives.verify(httpMethod, url, headers, publicJwk)
376
386
  ```
377
387
 
378
388
  </details>
@@ -381,7 +391,7 @@ const agent = await vestauth.tool.verify(req.method, url, req.headers)
381
391
 
382
392
  ## Available Tools
383
393
 
384
- > Vestauth is pioneering the auth layer for agents. Get in early on this distribution train. [Become a vestauth tool](mailto:mot@dotenvx.com)
394
+ > List of tools. We're actively building tools and looking for others to help grow the vestauth ecosystem with calls for tools like sending email, sms, uploading files and more. Vestauth agents need more tools. A tool is just a sharp API call. Add your tool here.
385
395
 
386
396
  * AS2 (Agentic Secret Storage) - https://as2.dotenvx.com
387
397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vestauth",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "description": "auth for agents–from the creator of dotenvx",
5
5
  "keywords": [
6
6
  "vestauth",
@@ -16,7 +16,7 @@ async function init () {
16
16
  logger.info(`• agent exists (${output.path}/AGENT_UID=${output.AGENT_UID})`)
17
17
  }
18
18
 
19
- logger.help('⮕ next run: [vestauth agent curl https://api.vestauth.com/whoami]')
19
+ logger.help(`⮕ next run: [vestauth agent curl ${output.AGENT_HOSTNAME}/whoami]`)
20
20
  } catch (error) {
21
21
  catchAndLog(error)
22
22
  process.exit(1)
@@ -10,12 +10,13 @@ class PostRegister {
10
10
  }
11
11
 
12
12
  async run () {
13
- const url = `${this.hostname}/register`
13
+ const hostname = this.hostname
14
+ const url = `${hostname}/register`
14
15
  const publicJwk = this.publicJwk
15
16
  const privateJwk = this.privateJwk
16
17
 
17
18
  const httpMethod = 'POST'
18
- const headers = await agentHeaders(httpMethod, url, 'REGISTERING', JSON.stringify(privateJwk))
19
+ const headers = await agentHeaders(httpMethod, url, 'REGISTERING', JSON.stringify(privateJwk), null, null, hostname)
19
20
  headers['Content-Type'] = 'application/json'
20
21
 
21
22
  const resp = await http(url, {
@@ -1,7 +1,7 @@
1
1
  const headers = require('./headers')
2
2
  const identity = require('./identity')
3
3
 
4
- async function agentHeaders (httpMethod, uri, uid = null, privateJwk = null, tag = 'web-bot-auth', nonce = null) {
4
+ async function agentHeaders (httpMethod, uri, uid = null, privateJwk = null, tag = 'web-bot-auth', nonce = null, hostname = null) {
5
5
  if (!privateJwk) {
6
6
  privateJwk = identity().privateJwk
7
7
  }
@@ -10,7 +10,7 @@ async function agentHeaders (httpMethod, uri, uid = null, privateJwk = null, tag
10
10
  uid = identity().uid
11
11
  }
12
12
 
13
- return await headers(httpMethod, uri, uid, privateJwk, tag, nonce)
13
+ return await headers(httpMethod, uri, uid, privateJwk, tag, nonce, hostname)
14
14
  }
15
15
 
16
16
  module.exports = agentHeaders
@@ -22,12 +22,13 @@ async function agentInit (hostname = null) {
22
22
  dotenvx.set('AGENT_PUBLIC_JWK', JSON.stringify(kp.publicJwk), { path: envPath, plain: true, quiet: true })
23
23
  dotenvx.set('AGENT_PRIVATE_JWK', JSON.stringify(kp.privateJwk), { path: envPath, plain: true, quiet: true })
24
24
  if (shouldPersistHostname) {
25
- dotenvx.set('AGENT_HOSTNAME', new URL(normalizedHostname).host, { path: envPath, plain: true, quiet: true })
25
+ dotenvx.set('AGENT_HOSTNAME', normalizedHostname, { path: envPath, plain: true, quiet: true })
26
26
  }
27
27
 
28
28
  return {
29
- AGENT_PUBLIC_JWK: kp.publicJwk,
30
29
  AGENT_UID: agent.uid,
30
+ AGENT_PUBLIC_JWK: kp.publicJwk,
31
+ AGENT_HOSTNAME: normalizedHostname,
31
32
  path: envPath,
32
33
  isNew: agent.is_new
33
34
  }
@@ -59,6 +59,15 @@ class Errors {
59
59
  return e
60
60
  }
61
61
 
62
+ missingSignatureInput () {
63
+ const code = 'MISSING_SIGNATURE_INPUT'
64
+ const message = `[${code}] missing --signature-input`
65
+
66
+ const e = new Error(message)
67
+ e.code = code
68
+ return e
69
+ }
70
+
62
71
  missingSignatureAgent () {
63
72
  const code = 'MISSING_SIGNATURE_AGENT'
64
73
  const message = `[${code}] missing --signature-agent`
@@ -4,12 +4,16 @@ const signatureParams = require('./signatureParams')
4
4
  const webBotAuthSignature = require('./webBotAuthSignature')
5
5
  const env = require('./env')
6
6
 
7
- function getAgentDiscoveryDomain () {
8
- const hostname = (env('AGENT_HOSTNAME') || process.env.AGENT_HOSTNAME || 'api.vestauth.com').trim().toLowerCase()
9
- return hostname.replace(/^https?:\/\//, '').split('/')[0]
7
+ function getAgentDiscoveryOrigin (hostname = null) {
8
+ if (!hostname) {
9
+ hostname = (env('AGENT_HOSTNAME') || process.env.AGENT_HOSTNAME || 'api.vestauth.com').trim()
10
+ }
11
+
12
+ const candidate = /^https?:\/\//i.test(hostname) ? hostname : `https://${hostname}`
13
+ return new URL(candidate).origin
10
14
  }
11
15
 
12
- async function headers (httpMethod, uri, uid, privateJwk, tag = 'web-bot-auth', nonce = null) {
16
+ async function headers (httpMethod, uri, uid, privateJwk, tag = 'web-bot-auth', nonce = null, hostname = null) {
13
17
  if (!uid) throw new Errors().missingUid()
14
18
  if (!privateJwk) throw new Errors().missingPrivateJwk()
15
19
 
@@ -27,12 +31,14 @@ async function headers (httpMethod, uri, uid, privateJwk, tag = 'web-bot-auth',
27
31
 
28
32
  const signatureInput = signatureParams(privateJwk.kid, tag, nonce)
29
33
  const signature = webBotAuthSignature(httpMethod, uri, signatureInput, privateJwk)
30
- const signatureAgent = `${uid}.${getAgentDiscoveryDomain()}` // no scheme; fqdn only
34
+ const discoveryOrigin = getAgentDiscoveryOrigin(hostname)
35
+ const discoveryUrl = new URL(discoveryOrigin)
36
+ const signatureAgent = `${discoveryUrl.protocol}//${uid}.${discoveryUrl.host}`
31
37
 
32
38
  return {
33
39
  Signature: `sig1=:${signature}:`,
34
40
  'Signature-Input': `sig1=${signatureInput}`,
35
- 'Signature-Agent': `sig1=${signatureAgent}`
41
+ 'Signature-Agent': `sig1="${signatureAgent}"`
36
42
  }
37
43
  }
38
44
 
@@ -0,0 +1,9 @@
1
+ function isEmptyObject (value) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
3
+ return false
4
+ }
5
+
6
+ return Object.keys(value).length === 0
7
+ }
8
+
9
+ module.exports = isEmptyObject
@@ -0,0 +1,6 @@
1
+ function isLocalhost (wellKnownUrl) {
2
+ const url = new URL(wellKnownUrl)
3
+ return url.hostname === 'localhost' || url.hostname.endsWith('.localhost')
4
+ }
5
+
6
+ module.exports = isLocalhost
@@ -2,21 +2,21 @@ const { parseDictionary } = require('structured-headers')
2
2
 
3
3
  // example: sig1=(\"@authority\");created=1769707366;keyid=\"xWuYtVgVV_ZQcfEWexUoln9ynA56PmfF4tAvWQ_Bf_o\";alg=\"ed25519\";expires=1769707666;nonce=\"RtdaKawQVJxEAJwCfI_5-7oVTmfjFkz-rGGifYZ2o2MdAMwJF2nYG3713rL1f9FJmPp8T2j4Sqcmh8H8p8TkRA\";tag=\"web-bot-auth\"
4
4
  function parseSignatureInputHeader (signatureInputHeader) {
5
+ if (signatureInputHeader == null) {
6
+ return {}
7
+ }
8
+
9
+ if (typeof signatureInputHeader !== 'string' || signatureInputHeader.trim() === '') {
10
+ return {}
11
+ }
12
+
5
13
  const dictionary = parseDictionary(signatureInputHeader)
6
14
  const entry = dictionary.entries().next()
7
- const [key, innerlist] = entry.value
8
- const [cwp, params] = innerlist
15
+ const [, innerlist] = entry.value
16
+ const [, params] = innerlist
9
17
  const values = Object.fromEntries(params)
10
- const components = []
11
- for (const entry of cwp) {
12
- components.push(entry[0])
13
- }
14
18
 
15
- return {
16
- key,
17
- values,
18
- components
19
- }
19
+ return values
20
20
  }
21
21
 
22
22
  module.exports = parseSignatureInputHeader
@@ -9,20 +9,29 @@ const authorityMessage = require('./authorityMessage')
9
9
  const publicJwkObject = require('./publicJwkObject')
10
10
  const verifyAgentFqdn = require('./verifyAgentFqdn')
11
11
  const Errors = require('./errors')
12
+ const isLocalhost = require('./isLocalhost')
13
+ const isEmptyObject = require('./isEmptyObject')
12
14
 
13
15
  async function resolvePublicJwk ({ signatureInput, signatureAgent, publicJwk }) {
14
16
  let uid
15
17
  let wellKnownUrl
16
18
 
17
- const { values } = parseSignatureInputHeader(signatureInput)
19
+ const values = parseSignatureInputHeader(signatureInput)
20
+ if (!values || isEmptyObject(values)) {
21
+ throw new Errors().missingSignatureInput()
22
+ }
23
+
18
24
  const kid = values.keyid
19
25
 
20
26
  if (signatureAgent) {
21
27
  const { value } = parseSignatureAgentHeader(signatureAgent)
22
- const fqdn = value
23
- verifyAgentFqdn(fqdn)
24
- const origin = `https://${fqdn}`
25
- uid = fqdn.split('.')[0]
28
+ const isUri = /^https?:\/\//i.test(value)
29
+ const origin = isUri ? new URL(value).origin : `https://${value}`
30
+ const hostname = new URL(origin).hostname
31
+ if (!isLocalhost(origin)) {
32
+ verifyAgentFqdn(hostname)
33
+ }
34
+ uid = hostname.split('.')[0]
26
35
  wellKnownUrl = `${origin}/.well-known/http-message-signatures-directory`
27
36
  }
28
37
 
@@ -33,7 +42,22 @@ async function resolvePublicJwk ({ signatureInput, signatureAgent, publicJwk })
33
42
  return { publicJwk: null }
34
43
  }
35
44
 
36
- const resp = await http(wellKnownUrl, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
45
+ let requestUrl = wellKnownUrl
46
+ const requestHeaders = {}
47
+
48
+ const url = new URL(requestUrl)
49
+ if (isLocalhost(wellKnownUrl)) {
50
+ const port = url.port || '80'
51
+ requestUrl = `http://127.0.0.1:${port}${url.pathname}`
52
+ requestHeaders.host = url.host
53
+ }
54
+
55
+ const opts = { method: 'GET' }
56
+ if (Object.keys(requestHeaders).length > 0) {
57
+ opts.headers = requestHeaders
58
+ }
59
+
60
+ const resp = await http(requestUrl, opts)
37
61
  if (resp.statusCode >= 400) {
38
62
  const json = await resp.body.json()
39
63
  throw buildApiError(resp.statusCode, json)
@@ -61,7 +85,11 @@ async function verify (httpMethod, uri, headers = {}, publicJwk) {
61
85
  const signatureInput = headers['Signature-Input'] || headers['signature-input']
62
86
  const signatureAgent = headers['Signature-Agent'] || headers['signature-agent']
63
87
 
64
- const { values } = parseSignatureInputHeader(signatureInput)
88
+ const values = parseSignatureInputHeader(signatureInput)
89
+ if (!values || isEmptyObject(values)) {
90
+ throw new Errors().missingSignatureInput()
91
+ }
92
+
65
93
  const { expires } = values
66
94
  if (expires && expires < (Math.floor(Date.now() / 1000))) {
67
95
  throw new Errors().expiredSignature()
@@ -16,7 +16,6 @@ function verifyAgentFqdn (fqdn) {
16
16
  if (!fqdn || typeof fqdn !== 'string') {
17
17
  throw new Errors().invalidSignatureAgent()
18
18
  }
19
-
20
19
  const pattern = getToolFqdnRegex()
21
20
  if (!pattern.test(fqdn)) {
22
21
  throw new Errors().invalidSignatureAgent()