@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 +13 -1
- package/package.json +4 -3
- package/src/cli/actions/gateway/start.js +17 -0
- package/src/cli/actions/settings/device.js +1 -1
- package/src/cli/commands/gateway.js +16 -0
- package/src/cli/dotenvx-ops.js +1 -1
- package/src/db/session.js +3 -8
- package/src/lib/main.js +6 -6
- package/src/lib/services/gatewayStart.js +132 -0
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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,
|
|
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
|
package/src/cli/dotenvx-ops.js
CHANGED
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') ||
|
|
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') ||
|
|
43
|
+
return this.store.get('DOTENVX_OPS_USERNAME') || undefined
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
token () {
|
|
47
|
-
return this.store.get('DOTENVX_OPS_TOKEN') ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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
|