@dotenvx/dotenvx-ops 0.30.3 → 0.31.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,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/dotenvx/dotenvx-ops/compare/v0.30.3...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx-ops/compare/v0.31.1...main)
6
+
7
+ ## [0.31.1](https://github.com/dotenvx/dotenvx-ops/compare/v0.31.0...v0.31.1) (2026-03-07)
8
+
9
+ ### Changed
10
+
11
+ * Shortened default showable device id for `dotenvx-ops settings device` ([#24](https://github.com/dotenvx/dotenvx-ops/pull/24))
12
+
13
+ ## [0.31.0](https://github.com/dotenvx/dotenvx-ops/compare/v0.30.3...v0.31.0) (2026-01-17)
14
+
15
+ ### Added
16
+
17
+ * Add `dotenvx-ops gateway start` (supports openai only currently) ([#22](https://github.com/dotenvx/dotenvx-ops/pull/22))
6
18
 
7
19
  ## [0.30.3](https://github.com/dotenvx/dotenvx-ops/compare/v0.30.2...v0.30.3) (2026-01-17)
8
20
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.30.3",
2
+ "version": "0.31.1",
3
3
  "name": "@dotenvx/dotenvx-ops",
4
4
  "description": "production grade dotenvx–with operational primitives",
5
5
  "author": "@motdotla",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@clack/core": "^0.4.2",
44
- "@dotenvx/dotenvx": "^1.48.1",
44
+ "@dotenvx/dotenvx": "^1.52.0",
45
45
  "@inquirer/prompts": "^7.10.1",
46
46
  "arch": "^2.1.1",
47
47
  "commander": "^11.1.0",
@@ -49,6 +49,7 @@
49
49
  "dotenv": "^17.2.0",
50
50
  "eciesjs": "^0.4.7",
51
51
  "execa": "^5.1.1",
52
+ "express": "^5.2.1",
52
53
  "open": "^8.4.2",
53
54
  "playwright": "^1.57.0",
54
55
  "systeminformation": "^5.22.11",
@@ -60,7 +61,7 @@
60
61
  "sinon": "^14.0.1",
61
62
  "standard": "^17.1.2",
62
63
  "standard-version": "^9.5.0",
63
- "tap": "^19.2.5"
64
+ "tap": "^21.6.2"
64
65
  },
65
66
  "publishConfig": {
66
67
  "access": "public"
@@ -0,0 +1,17 @@
1
+ const { logger } = require('@dotenvx/dotenvx')
2
+
3
+ const GatewayStart = require('./../../../lib/services/gatewayStart')
4
+
5
+ async function start () {
6
+ try {
7
+ const options = this.opts()
8
+ logger.debug(`options: ${JSON.stringify(options)}`)
9
+
10
+ await new GatewayStart({ port: options.port, hostname: options.hostname }).run()
11
+ } catch (error) {
12
+ logger.error(error.message)
13
+ process.exit(1)
14
+ }
15
+ }
16
+
17
+ module.exports = start
@@ -10,7 +10,7 @@ function device () {
10
10
  const sesh = new Session()
11
11
  const devicePublicKey = sesh.devicePublicKey()
12
12
  if (devicePublicKey && devicePublicKey.length > 1) {
13
- console.log(smartMask(devicePublicKey, options.unmask, 11))
13
+ console.log(smartMask(devicePublicKey, options.unmask, 6))
14
14
  } else {
15
15
  logger.error('missing device. Try generating one with [dotenvx-ops login].')
16
16
  process.exit(1)
@@ -0,0 +1,16 @@
1
+ const { Command } = require('commander')
2
+
3
+ const gateway = new Command('gateway')
4
+
5
+ gateway
6
+ .description('🛡️ gateway')
7
+ .allowUnknownOption()
8
+
9
+ // dotenvx-ops gateway start
10
+ const startAction = require('./../actions/gateway/start')
11
+ gateway
12
+ .command('start')
13
+ .description('start gateway')
14
+ .action(startAction)
15
+
16
+ module.exports = gateway
@@ -117,7 +117,7 @@ program
117
117
  .description('status')
118
118
  .action(statusAction)
119
119
 
120
- // dotenvx-ops settings
120
+ program.addCommand(require('./commands/gateway'))
121
121
  program.addCommand(require('./commands/settings'))
122
122
 
123
123
  // monkey-patch help output
package/src/db/session.js CHANGED
@@ -36,15 +36,15 @@ class Session {
36
36
  // Get
37
37
  //
38
38
  hostname () {
39
- return this.store.get('DOTENVX_OPS_HOSTNAME') || this.store.get('DOTENVX_RADAR_HOSTNAME') || 'https://ops.dotenvx.com'
39
+ return this.store.get('DOTENVX_OPS_HOSTNAME') || 'https://ops.dotenvx.com'
40
40
  }
41
41
 
42
42
  username () {
43
- return this.store.get('DOTENVX_OPS_USERNAME') || this.store.get('DOTENVX_RADAR_USERNAME') || undefined
43
+ return this.store.get('DOTENVX_OPS_USERNAME') || undefined
44
44
  }
45
45
 
46
46
  token () {
47
- return this.store.get('DOTENVX_OPS_TOKEN') || this.store.get('DOTENVX_RADAR_TOKEN') || undefined
47
+ return this.store.get('DOTENVX_OPS_TOKEN') || undefined
48
48
  }
49
49
 
50
50
  devicePublicKey () {
@@ -111,11 +111,6 @@ class Session {
111
111
  this.store.delete('DOTENVX_OPS_USERNAME')
112
112
  this.store.delete('DOTENVX_OPS_TOKEN')
113
113
  this.store.delete('DOTENVX_OPS_HOSTNAME')
114
- this.store.delete('DOTENVX_RADAR_USER')
115
- this.store.delete('DOTENVX_RADAR_USERNAME')
116
- this.store.delete('DOTENVX_RADAR_TOKEN')
117
- this.store.delete('DOTENVX_RADAR_HOSTNAME')
118
-
119
114
  return true
120
115
  }
121
116
  }
package/src/lib/main.js CHANGED
@@ -13,12 +13,12 @@ const dotenvxProjectId = require('./helpers/dotenvxProjectId')
13
13
  const observe = async function (encoded, options = {}) {
14
14
  const sesh = new Session() // TODO: handle scenario where constructor fails
15
15
 
16
- let hostname = process.env.DOTENVX_OPS_HOSTNAME || process.env.DOTENVX_RADAR_HOSTNAME || options.hostname
16
+ let hostname = process.env.DOTENVX_OPS_HOSTNAME || options.hostname
17
17
  if (!hostname) {
18
18
  hostname = sesh.hostname()
19
19
  }
20
20
 
21
- let token = process.env.DOTENVX_OPS_TOKEN || process.env.DOTENVX_RADAR_TOKEN || options.token
21
+ let token = process.env.DOTENVX_OPS_TOKEN || options.token
22
22
  if (!token) {
23
23
  token = sesh.token()
24
24
  }
@@ -44,12 +44,12 @@ const observe = async function (encoded, options = {}) {
44
44
  const get = async function (uri, options = {}) {
45
45
  const sesh = new Session() // TODO: handle scenario where constructor fails
46
46
 
47
- let hostname = process.env.DOTENVX_OPS_HOSTNAME || process.env.DOTENVX_RADAR_HOSTNAME || options.hostname
47
+ let hostname = process.env.DOTENVX_OPS_HOSTNAME || options.hostname
48
48
  if (!hostname) {
49
49
  hostname = sesh.hostname()
50
50
  }
51
51
 
52
- let token = process.env.DOTENVX_OPS_TOKEN || process.env.DOTENVX_RADAR_TOKEN || options.token
52
+ let token = process.env.DOTENVX_OPS_TOKEN || options.token
53
53
  if (!token) {
54
54
  token = sesh.token()
55
55
  }
@@ -61,12 +61,12 @@ const get = async function (uri, options = {}) {
61
61
  const set = async function (uri, value, options = {}) {
62
62
  const sesh = new Session() // TODO: handle scenario where constructor fails
63
63
 
64
- let hostname = process.env.DOTENVX_OPS_HOSTNAME || process.env.DOTENVX_RADAR_HOSTNAME || options.hostname
64
+ let hostname = process.env.DOTENVX_OPS_HOSTNAME || options.hostname
65
65
  if (!hostname) {
66
66
  hostname = sesh.hostname()
67
67
  }
68
68
 
69
- let token = process.env.DOTENVX_OPS_TOKEN || process.env.DOTENVX_RADAR_TOKEN || options.token
69
+ let token = process.env.DOTENVX_OPS_TOKEN || options.token
70
70
  if (!token) {
71
71
  token = sesh.token()
72
72
  }
@@ -0,0 +1,132 @@
1
+ const dotenvx = require('@dotenvx/dotenvx')
2
+ const { logger } = dotenvx
3
+ const express = require('express')
4
+ const { randomUUID } = require('node:crypto')
5
+ const { Readable } = require('node:stream')
6
+
7
+ class GatewayStart {
8
+ constructor (options = {}) {
9
+ this.port = Number(options.port) || 7278
10
+ this.hostname = options.hostname || '127.0.0.1'
11
+ this.upstreamBaseUrl = options.upstreamBaseUrl || 'https://api.openai.com/v1'
12
+ }
13
+
14
+ async run () {
15
+ dotenvx.config()
16
+
17
+ const app = express()
18
+ app.use(express.json({ limit: '10mb' }))
19
+ app.use((req, res, next) => {
20
+ const startedAt = process.hrtime.bigint()
21
+ const requestId = req.headers['x-request-id'] || randomUUID()
22
+ const fwd = req.headers['x-forwarded-for'] || req.socket.remoteAddress || '-'
23
+ const host = req.headers.host || `${this.hostname}:${this.port}`
24
+ const path = req.originalUrl || req.url
25
+
26
+ res.setHeader('x-request-id', requestId)
27
+
28
+ res.on('finish', () => {
29
+ const service = Math.max(1, Math.round(Number(process.hrtime.bigint() - startedAt) / 1e6))
30
+ const bytes = res.getHeader('content-length') || '-'
31
+ const protocol = `http/${req.httpVersion}`
32
+
33
+ console.log(
34
+ `at=info method=${req.method} path="${path}" host=${host} request_id=${requestId} ` +
35
+ `fwd="${fwd}" dyno=web.1 connect=0ms service=${service}ms status=${res.statusCode} bytes=${bytes} protocol=${protocol}`
36
+ )
37
+ })
38
+
39
+ next()
40
+ })
41
+
42
+ app.get('/', (req, res) => {
43
+ res.json({ service: 'dotenvx gateway', ok: true })
44
+ })
45
+
46
+ // Optional: health
47
+ app.get('/healthz', (req, res) => res.status(200).json({ ok: true }))
48
+
49
+ // OpenAI Responses API (required for pi openai defaults)
50
+ app.post('/v1/responses', (req, res) => this.handleProxy(req, res, '/responses'))
51
+
52
+ // Optional: OpenAI Chat Completions compatibility
53
+ app.post('/v1/chat/completions', (req, res) => this.handleProxy(req, res, '/chat/completions'))
54
+
55
+ // Optional: models passthrough
56
+ app.get('/v1/models', (req, res) => this.handleProxy(req, res, '/models'))
57
+
58
+ return await new Promise((resolve, reject) => {
59
+ const server = app.listen(this.port, this.hostname, () => {
60
+ logger.successv(`dotenvx gateway listening on http://${this.hostname}:${this.port}`)
61
+ resolve(server)
62
+ })
63
+ server.on('error', reject)
64
+ })
65
+ }
66
+
67
+ async handleProxy (req, res, upstreamPath) {
68
+ try {
69
+ // 1) Authenticate caller token (gateway token / vestauth token)
70
+ const auth = req.headers.authorization || ''
71
+ const token = auth.startsWith('Bearer ') ? auth.slice(7) : null
72
+ if (!token) return res.status(401).json({ error: { message: 'Missing bearer token' } })
73
+
74
+ // TODO: replace with real vestauth verification
75
+ // const subject = await this.verifyGatewayToken(token)
76
+ // if (!subject) return res.status(403).json({ error: { message: 'Invalid token' } })
77
+
78
+ // 2) Fetch provider secret just-in-time (dotenvx/as2)
79
+ const openaiApiKey = await this.getOpenAIKeyForSubject()
80
+ if (!openaiApiKey) return res.status(500).json({ error: { message: 'No upstream key available' } })
81
+
82
+ // 3) Forward request upstream
83
+ const upstreamUrl = `${this.upstreamBaseUrl}${upstreamPath}`
84
+
85
+ const upstreamResp = await fetch(upstreamUrl, {
86
+ method: req.method,
87
+ headers: {
88
+ 'content-type': 'application/json',
89
+ authorization: `Bearer ${openaiApiKey}`
90
+ },
91
+ body: req.method === 'GET' ? undefined : JSON.stringify(req.body)
92
+ })
93
+
94
+ // 4) Pass through status + relevant headers
95
+ res.status(upstreamResp.status)
96
+ const contentType = upstreamResp.headers.get('content-type') || 'application/json'
97
+ res.setHeader('content-type', contentType)
98
+
99
+ const requestId = upstreamResp.headers.get('x-request-id')
100
+ if (requestId) res.setHeader('x-request-id', requestId)
101
+
102
+ // 5) Stream SSE directly when needed
103
+ if (contentType.includes('text/event-stream') && upstreamResp.body) {
104
+ res.setHeader('cache-control', 'no-cache')
105
+ res.setHeader('connection', 'keep-alive')
106
+ Readable.fromWeb(upstreamResp.body).pipe(res)
107
+ return
108
+ }
109
+
110
+ // 6) Non-stream response
111
+ const text = await upstreamResp.text()
112
+ res.send(text)
113
+ } catch (err) {
114
+ logger.error(`gateway proxy error: ${err?.message || err}`)
115
+ res.status(500).json({ error: { message: 'Gateway proxy failed' } })
116
+ }
117
+ }
118
+
119
+ async verifyGatewayToken (token) {
120
+ // TODO: verify with vestauth/dotenvx auth
121
+ // return subject string (e.g. user/org id) if valid
122
+ return token ? 'vestauth:user:123' : null
123
+ }
124
+
125
+ async getOpenAIKeyForSubject (subject = null) {
126
+ // TODO: call dotenvx.com/as2 with subject context and fetch secret JIT
127
+ // IMPORTANT: never log this value
128
+ return process.env.OPENAI_API_KEY // placeholder for initial test only
129
+ }
130
+ }
131
+
132
+ module.exports = GatewayStart