@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 +13 -1
- package/package.json +4 -3
- package/src/cli/actions/gateway/start.js +17 -0
- package/src/cli/actions/get.js +1 -1
- package/src/cli/actions/settings/device.js +1 -1
- package/src/cli/actions/settings/hostname.js +1 -1
- package/src/cli/actions/settings/path.js +1 -1
- package/src/cli/actions/settings/token.js +1 -1
- package/src/cli/actions/settings/username.js +1 -1
- package/src/cli/actions/status.js +1 -1
- package/src/cli/commands/gateway.js +16 -0
- package/src/cli/dotenvx-ops.js +1 -1
- 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.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.
|
|
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.
|
|
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
|
package/src/cli/actions/get.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
@@ -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
|