@dotenvx/dotenvx-ops 0.30.2 → 0.31.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/dotenvx/dotenvx-ops/compare/v0.30.2...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx-ops/compare/v0.31.0...main)
6
+
7
+ ## [0.31.0](https://github.com/dotenvx/dotenvx-ops/compare/v0.30.3...v0.31.0) (2026-01-17)
8
+
9
+ ### Added
10
+
11
+ * Add `dotenvx-ops gateway start` (supports openai only currently) ([#22](https://github.com/dotenvx/dotenvx-ops/pull/22))
12
+
13
+ ## [0.30.3](https://github.com/dotenvx/dotenvx-ops/compare/v0.30.2...v0.30.3) (2026-01-17)
14
+
15
+ ### Changed
16
+
17
+ * Switch to `console.log` ([#21](https://github.com/dotenvx/dotenvx-ops/pull/21) [dotenvx#386](https://github.com/dotenvx/dotenvx/issues/386))
6
18
 
7
19
  ## [0.30.2](https://github.com/dotenvx/dotenvx-ops/compare/v0.30.1...v0.30.2) (2026-01-13)
8
20
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.30.2",
2
+ "version": "0.31.0",
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
@@ -12,7 +12,7 @@ async function get (uri) {
12
12
 
13
13
  try {
14
14
  const value = await main.get(uri, { hostname, token })
15
- process.stdout.write(value)
15
+ console.log(value)
16
16
  } catch (error) {
17
17
  if (error.message) {
18
18
  logger.error(error.message)
@@ -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
- process.stdout.write(smartMask(devicePublicKey, options.unmask, 11))
13
+ console.log(smartMask(devicePublicKey, options.unmask, 11))
14
14
  } else {
15
15
  logger.error('missing device. Try generating one with [dotenvx-ops login].')
16
16
  process.exit(1)
@@ -6,7 +6,7 @@ function hostname () {
6
6
  const sesh = new Session()
7
7
  const _hostname = sesh.hostname()
8
8
  if (_hostname && _hostname.length > 1) {
9
- process.stdout.write(_hostname)
9
+ console.log(_hostname)
10
10
  } else {
11
11
  logger.error('missing hostname. Try running [dotenvx-ops login].')
12
12
  process.exit(1)
@@ -7,7 +7,7 @@ function path () {
7
7
  const sesh = new Session()
8
8
  const path = sesh.path()
9
9
  if (path && path.length > 1) {
10
- process.stdout.write(path)
10
+ console.log(path)
11
11
  } else {
12
12
  logger.error('missing path. Try generating one with [dotenvx-ops login].')
13
13
  process.exit(1)
@@ -10,7 +10,7 @@ function token () {
10
10
  const sesh = new Session()
11
11
  const token = sesh.token()
12
12
  if (token && token.length > 1) {
13
- process.stdout.write(smartMask(token, options.unmask, 11))
13
+ console.log(smartMask(token, options.unmask, 11))
14
14
  } else {
15
15
  logger.error('missing token. Try generating one with [dotenvx-ops login].')
16
16
  process.exit(1)
@@ -8,7 +8,7 @@ async function username () {
8
8
  const username = sesh.username()
9
9
 
10
10
  if (username) {
11
- process.stdout.write(username)
11
+ console.log(username)
12
12
  } else {
13
13
  logger.error('login required. Try running [dotenvx-pro login].')
14
14
  process.exit(1)
@@ -7,7 +7,7 @@ async function status () {
7
7
  logger.debug(`options: ${JSON.stringify(options)}`)
8
8
 
9
9
  const { status } = new Status().run()
10
- process.stdout.write(status)
10
+ console.log(status)
11
11
  }
12
12
 
13
13
  module.exports = status
@@ -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
@@ -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