humanenv 0.1.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/README.md +303 -0
- package/package.json +38 -0
- package/packages/cli/package.json +15 -0
- package/packages/cli/src/bin.js +228 -0
- package/packages/client/bin.js +174 -0
- package/packages/client/build.js +57 -0
- package/packages/client/dist/cli.js +1041 -0
- package/packages/client/dist/index.cjs +333 -0
- package/packages/client/dist/index.mjs +296 -0
- package/packages/client/package.json +24 -0
- package/packages/client/src/cli/bin.js +228 -0
- package/packages/client/src/cli/entry.js +465 -0
- package/packages/client/src/index.ts +31 -0
- package/packages/client/src/shared/buffer-shim.d.ts +4 -0
- package/packages/client/src/shared/crypto.ts +98 -0
- package/packages/client/src/shared/errors.ts +32 -0
- package/packages/client/src/shared/index.ts +3 -0
- package/packages/client/src/shared/types.ts +118 -0
- package/packages/client/src/ws-manager.ts +263 -0
- package/packages/server/package.json +21 -0
- package/packages/server/src/auth.ts +13 -0
- package/packages/server/src/db/index.ts +19 -0
- package/packages/server/src/db/interface.ts +33 -0
- package/packages/server/src/db/mongo.ts +166 -0
- package/packages/server/src/db/sqlite.ts +180 -0
- package/packages/server/src/index.ts +123 -0
- package/packages/server/src/pk-manager.ts +79 -0
- package/packages/server/src/routes/index.ts +110 -0
- package/packages/server/src/views/index.ejs +359 -0
- package/packages/server/src/ws/router.ts +263 -0
- package/packages/shared/package.json +13 -0
- package/packages/shared/src/buffer-shim.d.ts +4 -0
- package/packages/shared/src/crypto.ts +98 -0
- package/packages/shared/src/errors.ts +32 -0
- package/packages/shared/src/index.ts +3 -0
- package/packages/shared/src/types.ts +119 -0
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/shared/errors.ts
|
|
35
|
+
var ErrorCode, ErrorMessages, HumanEnvError;
|
|
36
|
+
var init_errors = __esm({
|
|
37
|
+
"src/shared/errors.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
40
|
+
ErrorCode2["SERVER_PK_NOT_AVAILABLE"] = "SERVER_PK_NOT_AVAILABLE";
|
|
41
|
+
ErrorCode2["CLIENT_AUTH_INVALID_PROJECT_NAME"] = "CLIENT_AUTH_INVALID_PROJECT_NAME";
|
|
42
|
+
ErrorCode2["CLIENT_AUTH_NOT_WHITELISTED"] = "CLIENT_AUTH_NOT_WHITELISTED";
|
|
43
|
+
ErrorCode2["CLIENT_AUTH_INVALID_API_KEY"] = "CLIENT_AUTH_INVALID_API_KEY";
|
|
44
|
+
ErrorCode2["CLIENT_CONN_MAX_RETRIES_EXCEEDED"] = "CLIENT_CONN_MAX_RETRIES_EXCEEDED";
|
|
45
|
+
ErrorCode2["ENV_API_MODE_ONLY"] = "ENV_API_MODE_ONLY";
|
|
46
|
+
ErrorCode2["SERVER_INTERNAL_ERROR"] = "SERVER_INTERNAL_ERROR";
|
|
47
|
+
ErrorCode2["WS_CONNECTION_FAILED"] = "WS_CONNECTION_FAILED";
|
|
48
|
+
ErrorCode2["DB_OPERATION_FAILED"] = "DB_OPERATION_FAILED";
|
|
49
|
+
return ErrorCode2;
|
|
50
|
+
})(ErrorCode || {});
|
|
51
|
+
ErrorMessages = {
|
|
52
|
+
SERVER_PK_NOT_AVAILABLE: "Server private key is not available. Restart pending.",
|
|
53
|
+
CLIENT_AUTH_INVALID_PROJECT_NAME: "Invalid or unknown project name.",
|
|
54
|
+
CLIENT_AUTH_NOT_WHITELISTED: "Client fingerprint is not whitelisted for this project.",
|
|
55
|
+
CLIENT_AUTH_INVALID_API_KEY: "Invalid or expired API key.",
|
|
56
|
+
CLIENT_CONN_MAX_RETRIES_EXCEEDED: "Maximum WS connection retries exceeded.",
|
|
57
|
+
ENV_API_MODE_ONLY: "This env is API-mode only and cannot be accessed via CLI.",
|
|
58
|
+
SERVER_INTERNAL_ERROR: "An internal server error occurred.",
|
|
59
|
+
WS_CONNECTION_FAILED: "Failed to establish WebSocket connection.",
|
|
60
|
+
DB_OPERATION_FAILED: "Database operation failed."
|
|
61
|
+
};
|
|
62
|
+
HumanEnvError = class extends Error {
|
|
63
|
+
code;
|
|
64
|
+
constructor(code, message) {
|
|
65
|
+
super(message ?? ErrorMessages[code]);
|
|
66
|
+
this.name = "HumanEnvError";
|
|
67
|
+
this.code = code;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/shared/types.ts
|
|
74
|
+
var SKILL_CONTENT;
|
|
75
|
+
var init_types = __esm({
|
|
76
|
+
"src/shared/types.ts"() {
|
|
77
|
+
"use strict";
|
|
78
|
+
SKILL_CONTENT = `---
|
|
79
|
+
name: humanenv-usage
|
|
80
|
+
description: Use humanenv to retrieve sensitives/envs. Never log/read sensitives from humanenv. They are envs for humans only.
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## How to retrieve sensitives
|
|
84
|
+
|
|
85
|
+
### JavaScript API
|
|
86
|
+
Use \`humanenv.get('key')\` inside your application. The returned value must be used immediately and then set to \`null\` to prevent memory leaks.
|
|
87
|
+
|
|
88
|
+
### CLI (non-TTY mode)
|
|
89
|
+
Only non-API-mode envs are accessible. Do not attempt to use this to log or dump all envs.
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
1. NEVER log env values retrieved from humanenv
|
|
93
|
+
2. NEVER dump or export multiple values at once
|
|
94
|
+
3. ALWAYS null variables after use
|
|
95
|
+
4. NEVER write sensitives to files
|
|
96
|
+
5. Do not generate scripts that use humanenv in loops or to exfiltrate data`;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// src/shared/crypto.ts
|
|
101
|
+
function generateMnemonic() {
|
|
102
|
+
const entropy = crypto.randomBytes(16);
|
|
103
|
+
const words = [];
|
|
104
|
+
for (let i = 0; i < 32; i++) {
|
|
105
|
+
words.push(BIP39_WORDLIST[entropy[i] % BIP39_WORDLIST.length]);
|
|
106
|
+
}
|
|
107
|
+
return words.slice(0, 12).join(" ");
|
|
108
|
+
}
|
|
109
|
+
function validateMnemonic(mnemonic) {
|
|
110
|
+
const words = mnemonic.trim().toLowerCase().split(/\s+/);
|
|
111
|
+
if (words.length !== 12) return false;
|
|
112
|
+
return words.every((w) => BIP39_WORDLIST.includes(w));
|
|
113
|
+
}
|
|
114
|
+
function derivePkFromMnemonic(mnemonic) {
|
|
115
|
+
return crypto.pbkdf2Sync(
|
|
116
|
+
mnemonic.toLowerCase().trim(),
|
|
117
|
+
"humanenv-server-v1",
|
|
118
|
+
PBKDF2_ITERATIONS,
|
|
119
|
+
PK_KEY_LENGTH,
|
|
120
|
+
"sha256"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
function hashPkForVerification(pk) {
|
|
124
|
+
return crypto.createHash("sha256").update(pk).digest("hex");
|
|
125
|
+
}
|
|
126
|
+
function encryptWithPk(value, pk, aad) {
|
|
127
|
+
const iv = crypto.randomBytes(12);
|
|
128
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", pk, iv);
|
|
129
|
+
cipher.setAAD(crypto.createHash("sha256").update(aad).digest());
|
|
130
|
+
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
131
|
+
const tag = cipher.getAuthTag();
|
|
132
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
133
|
+
}
|
|
134
|
+
function decryptWithPk(encryptedBase64, pk, aad) {
|
|
135
|
+
const buf = Buffer.from(encryptedBase64, "base64");
|
|
136
|
+
const iv = buf.subarray(0, 12);
|
|
137
|
+
const tag = buf.subarray(12, 28);
|
|
138
|
+
const ciphertext = buf.subarray(28);
|
|
139
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", pk, iv);
|
|
140
|
+
decipher.setAAD(crypto.createHash("sha256").update(aad).digest());
|
|
141
|
+
decipher.setAuthTag(tag);
|
|
142
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
143
|
+
return decrypted.toString("utf8");
|
|
144
|
+
}
|
|
145
|
+
function generateFingerprint() {
|
|
146
|
+
const components = [
|
|
147
|
+
process.env.HOSTNAME || "unknown-host",
|
|
148
|
+
process.platform,
|
|
149
|
+
process.arch,
|
|
150
|
+
process.version
|
|
151
|
+
];
|
|
152
|
+
return crypto.createHash("sha256").update(components.join("|")).digest("hex").slice(0, 16);
|
|
153
|
+
}
|
|
154
|
+
var crypto, PBKDF2_ITERATIONS, PK_KEY_LENGTH, BIP39_WORDLIST;
|
|
155
|
+
var init_crypto = __esm({
|
|
156
|
+
"src/shared/crypto.ts"() {
|
|
157
|
+
"use strict";
|
|
158
|
+
crypto = __toESM(require("node:crypto"));
|
|
159
|
+
PBKDF2_ITERATIONS = 1e5;
|
|
160
|
+
PK_KEY_LENGTH = 32;
|
|
161
|
+
BIP39_WORDLIST = [
|
|
162
|
+
"abandon",
|
|
163
|
+
"ability",
|
|
164
|
+
"able",
|
|
165
|
+
"about",
|
|
166
|
+
"above",
|
|
167
|
+
"absent",
|
|
168
|
+
"absorb",
|
|
169
|
+
"abstract",
|
|
170
|
+
"absurd",
|
|
171
|
+
"abuse",
|
|
172
|
+
"access",
|
|
173
|
+
"accident",
|
|
174
|
+
"account",
|
|
175
|
+
"accuse",
|
|
176
|
+
"achieve",
|
|
177
|
+
"acid",
|
|
178
|
+
"acoustic",
|
|
179
|
+
"acquire",
|
|
180
|
+
"across",
|
|
181
|
+
"act",
|
|
182
|
+
"action",
|
|
183
|
+
"actor",
|
|
184
|
+
"actress",
|
|
185
|
+
"actual",
|
|
186
|
+
"adapt",
|
|
187
|
+
"add",
|
|
188
|
+
"addict",
|
|
189
|
+
"address",
|
|
190
|
+
"adjust",
|
|
191
|
+
"admit",
|
|
192
|
+
"adult",
|
|
193
|
+
"advance",
|
|
194
|
+
"advice",
|
|
195
|
+
"aerobic",
|
|
196
|
+
"affair",
|
|
197
|
+
"afford",
|
|
198
|
+
"afraid",
|
|
199
|
+
"again",
|
|
200
|
+
"age",
|
|
201
|
+
"agent",
|
|
202
|
+
"agree",
|
|
203
|
+
"ahead",
|
|
204
|
+
"aim",
|
|
205
|
+
"air",
|
|
206
|
+
"airport",
|
|
207
|
+
"aisle",
|
|
208
|
+
"alarm",
|
|
209
|
+
"album",
|
|
210
|
+
"alcohol",
|
|
211
|
+
"alert",
|
|
212
|
+
"alien",
|
|
213
|
+
"all",
|
|
214
|
+
"alley",
|
|
215
|
+
"allow",
|
|
216
|
+
"almost",
|
|
217
|
+
"alone",
|
|
218
|
+
"alpha",
|
|
219
|
+
"already",
|
|
220
|
+
"also",
|
|
221
|
+
"alter",
|
|
222
|
+
"always",
|
|
223
|
+
"amateur",
|
|
224
|
+
"amazing",
|
|
225
|
+
"among",
|
|
226
|
+
"amount",
|
|
227
|
+
"amused",
|
|
228
|
+
"analyst",
|
|
229
|
+
"anchor",
|
|
230
|
+
"ancient",
|
|
231
|
+
"anger",
|
|
232
|
+
"angle",
|
|
233
|
+
"angry",
|
|
234
|
+
"animal",
|
|
235
|
+
"ankle",
|
|
236
|
+
"announce",
|
|
237
|
+
"annual",
|
|
238
|
+
"another",
|
|
239
|
+
"answer",
|
|
240
|
+
"antenna",
|
|
241
|
+
"antique",
|
|
242
|
+
"anxiety",
|
|
243
|
+
"any",
|
|
244
|
+
"apart",
|
|
245
|
+
"apology",
|
|
246
|
+
"appear",
|
|
247
|
+
"apple",
|
|
248
|
+
"approve",
|
|
249
|
+
"april",
|
|
250
|
+
"arch",
|
|
251
|
+
"arctic",
|
|
252
|
+
"area",
|
|
253
|
+
"arena",
|
|
254
|
+
"argue",
|
|
255
|
+
"arm",
|
|
256
|
+
"armed",
|
|
257
|
+
"armor",
|
|
258
|
+
"army",
|
|
259
|
+
"around",
|
|
260
|
+
"arrest",
|
|
261
|
+
"arrive",
|
|
262
|
+
"arrow",
|
|
263
|
+
"art",
|
|
264
|
+
"artist",
|
|
265
|
+
"artwork",
|
|
266
|
+
"ask",
|
|
267
|
+
"aspect",
|
|
268
|
+
"assault",
|
|
269
|
+
"asset",
|
|
270
|
+
"assist",
|
|
271
|
+
"assume",
|
|
272
|
+
"asthma",
|
|
273
|
+
"athlete",
|
|
274
|
+
"atom",
|
|
275
|
+
"attack",
|
|
276
|
+
"attend",
|
|
277
|
+
"attitude",
|
|
278
|
+
"attract",
|
|
279
|
+
"auction",
|
|
280
|
+
"audit",
|
|
281
|
+
"august",
|
|
282
|
+
"aunt",
|
|
283
|
+
"author",
|
|
284
|
+
"auto",
|
|
285
|
+
"autumn",
|
|
286
|
+
"average",
|
|
287
|
+
"avocado",
|
|
288
|
+
"avoid",
|
|
289
|
+
"awake",
|
|
290
|
+
"aware",
|
|
291
|
+
"awesome",
|
|
292
|
+
"awful",
|
|
293
|
+
"awkward",
|
|
294
|
+
"axis",
|
|
295
|
+
"baby",
|
|
296
|
+
"bachelor",
|
|
297
|
+
"bacon",
|
|
298
|
+
"badge",
|
|
299
|
+
"bag",
|
|
300
|
+
"balance",
|
|
301
|
+
"balcony",
|
|
302
|
+
"ball",
|
|
303
|
+
"bamboo",
|
|
304
|
+
"banana",
|
|
305
|
+
"banner",
|
|
306
|
+
"bar",
|
|
307
|
+
"barely",
|
|
308
|
+
"bargain",
|
|
309
|
+
"barrel",
|
|
310
|
+
"base",
|
|
311
|
+
"basic",
|
|
312
|
+
"basket",
|
|
313
|
+
"battle",
|
|
314
|
+
"beach",
|
|
315
|
+
"bean",
|
|
316
|
+
"beauty",
|
|
317
|
+
"because",
|
|
318
|
+
"become",
|
|
319
|
+
"beef",
|
|
320
|
+
"before",
|
|
321
|
+
"begin",
|
|
322
|
+
"behave",
|
|
323
|
+
"behind",
|
|
324
|
+
"believe",
|
|
325
|
+
"below",
|
|
326
|
+
"bench",
|
|
327
|
+
"benefit",
|
|
328
|
+
"best",
|
|
329
|
+
"betray",
|
|
330
|
+
"better",
|
|
331
|
+
"between",
|
|
332
|
+
"beyond",
|
|
333
|
+
"bicycle",
|
|
334
|
+
"bid",
|
|
335
|
+
"bike",
|
|
336
|
+
"bind",
|
|
337
|
+
"biology",
|
|
338
|
+
"bird",
|
|
339
|
+
"birth",
|
|
340
|
+
"bitter",
|
|
341
|
+
"black",
|
|
342
|
+
"blade",
|
|
343
|
+
"blame",
|
|
344
|
+
"blanket",
|
|
345
|
+
"blast"
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// src/shared/index.ts
|
|
351
|
+
var shared_exports = {};
|
|
352
|
+
__export(shared_exports, {
|
|
353
|
+
ErrorCode: () => ErrorCode,
|
|
354
|
+
ErrorMessages: () => ErrorMessages,
|
|
355
|
+
HumanEnvError: () => HumanEnvError,
|
|
356
|
+
SKILL_CONTENT: () => SKILL_CONTENT,
|
|
357
|
+
decryptWithPk: () => decryptWithPk,
|
|
358
|
+
derivePkFromMnemonic: () => derivePkFromMnemonic,
|
|
359
|
+
encryptWithPk: () => encryptWithPk,
|
|
360
|
+
generateFingerprint: () => generateFingerprint,
|
|
361
|
+
generateMnemonic: () => generateMnemonic,
|
|
362
|
+
hashPkForVerification: () => hashPkForVerification,
|
|
363
|
+
validateMnemonic: () => validateMnemonic
|
|
364
|
+
});
|
|
365
|
+
var init_shared = __esm({
|
|
366
|
+
"src/shared/index.ts"() {
|
|
367
|
+
"use strict";
|
|
368
|
+
init_errors();
|
|
369
|
+
init_types();
|
|
370
|
+
init_crypto();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// src/ws-manager.ts
|
|
375
|
+
var ws_manager_exports = {};
|
|
376
|
+
__export(ws_manager_exports, {
|
|
377
|
+
HumanEnvClient: () => HumanEnvClient
|
|
378
|
+
});
|
|
379
|
+
var import_ws, HumanEnvClient;
|
|
380
|
+
var init_ws_manager = __esm({
|
|
381
|
+
"src/ws-manager.ts"() {
|
|
382
|
+
"use strict";
|
|
383
|
+
init_shared();
|
|
384
|
+
import_ws = __toESM(require("ws"));
|
|
385
|
+
HumanEnvClient = class {
|
|
386
|
+
ws = null;
|
|
387
|
+
connected = false;
|
|
388
|
+
authenticated = false;
|
|
389
|
+
_whitelistStatus = null;
|
|
390
|
+
attempts = 0;
|
|
391
|
+
pending = /* @__PURE__ */ new Map();
|
|
392
|
+
config;
|
|
393
|
+
retryTimer = null;
|
|
394
|
+
pingTimer = null;
|
|
395
|
+
reconnecting = false;
|
|
396
|
+
disconnecting = false;
|
|
397
|
+
_authResolve = null;
|
|
398
|
+
_authReject = null;
|
|
399
|
+
get whitelistStatus() {
|
|
400
|
+
return this._whitelistStatus;
|
|
401
|
+
}
|
|
402
|
+
constructor(config) {
|
|
403
|
+
this.config = {
|
|
404
|
+
serverUrl: config.serverUrl,
|
|
405
|
+
projectName: config.projectName,
|
|
406
|
+
projectApiKey: config.projectApiKey || "",
|
|
407
|
+
maxRetries: config.maxRetries ?? 10
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
getFingerprint() {
|
|
411
|
+
return generateFingerprint();
|
|
412
|
+
}
|
|
413
|
+
async connect() {
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
this.doConnect(resolve, reject);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
doConnect(resolve, reject) {
|
|
419
|
+
const proto = this.config.serverUrl.startsWith("https") ? "wss" : "ws";
|
|
420
|
+
const host = this.config.serverUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
421
|
+
const url = `${proto}://${host}/ws`;
|
|
422
|
+
this.ws = new import_ws.default(url);
|
|
423
|
+
this.ws.on("open", () => {
|
|
424
|
+
this.connected = true;
|
|
425
|
+
this.attempts = 0;
|
|
426
|
+
this.reconnecting = false;
|
|
427
|
+
this.startPing();
|
|
428
|
+
this.sendAuth(resolve, reject);
|
|
429
|
+
});
|
|
430
|
+
this.ws.on("message", (raw) => {
|
|
431
|
+
try {
|
|
432
|
+
const msg = JSON.parse(raw.toString());
|
|
433
|
+
this.handleMessage(msg);
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
this.ws.on("close", () => {
|
|
438
|
+
this.connected = false;
|
|
439
|
+
this.authenticated = false;
|
|
440
|
+
this.stopPing();
|
|
441
|
+
if (!this.disconnecting && !this.reconnecting) this.scheduleReconnect(reject);
|
|
442
|
+
});
|
|
443
|
+
this.ws.on("error", () => {
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
sendAuth(resolve, reject) {
|
|
447
|
+
this._authResolve = resolve;
|
|
448
|
+
this._authReject = reject;
|
|
449
|
+
this.ws?.send(JSON.stringify({
|
|
450
|
+
type: "auth",
|
|
451
|
+
payload: {
|
|
452
|
+
projectName: this.config.projectName,
|
|
453
|
+
apiKey: this.config.projectApiKey,
|
|
454
|
+
fingerprint: this.getFingerprint()
|
|
455
|
+
}
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
handleMessage(msg) {
|
|
459
|
+
if (msg.type === "auth_response") {
|
|
460
|
+
if (msg.payload.success) {
|
|
461
|
+
this.authenticated = true;
|
|
462
|
+
this._whitelistStatus = msg.payload.status || (msg.payload.whitelisted ? "approved" : "pending");
|
|
463
|
+
this._authResolve?.();
|
|
464
|
+
} else {
|
|
465
|
+
this._authReject?.(new HumanEnvError(msg.payload.code, msg.payload.error));
|
|
466
|
+
}
|
|
467
|
+
this._authResolve = null;
|
|
468
|
+
this._authReject = null;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (msg.type === "get_response") {
|
|
472
|
+
this._resolvePending("get", msg.payload);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (msg.type === "set_response") {
|
|
476
|
+
this._resolvePending("set", msg.payload);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (msg.type === "pong") {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
_resolvePending(kind, payload) {
|
|
483
|
+
for (const [id, op] of this.pending) {
|
|
484
|
+
clearTimeout(op.timeout);
|
|
485
|
+
this.pending.delete(id);
|
|
486
|
+
if (payload.error) {
|
|
487
|
+
op.reject(new HumanEnvError(payload.code, payload.error));
|
|
488
|
+
} else {
|
|
489
|
+
op.resolve(payload);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async get(keyOrKeys) {
|
|
495
|
+
if (!this.connected || !this.authenticated) throw new HumanEnvError("CLIENT_AUTH_INVALID_API_KEY" /* CLIENT_AUTH_INVALID_API_KEY */);
|
|
496
|
+
if (Array.isArray(keyOrKeys)) {
|
|
497
|
+
const result = {};
|
|
498
|
+
await Promise.all(keyOrKeys.map(async (key) => {
|
|
499
|
+
result[key] = await this._getSingle(key);
|
|
500
|
+
}));
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
return this._getSingle(keyOrKeys);
|
|
504
|
+
}
|
|
505
|
+
_getSingle(key) {
|
|
506
|
+
return new Promise((resolve, reject) => {
|
|
507
|
+
const msgId = `${key}-${Date.now()}`;
|
|
508
|
+
const timeout = setTimeout(() => {
|
|
509
|
+
this.pending.delete(msgId);
|
|
510
|
+
reject(new Error(`Timeout getting env: ${key}`));
|
|
511
|
+
}, 8e3);
|
|
512
|
+
this.pending.set(msgId, { resolve: (v) => resolve(v.value), reject, timeout });
|
|
513
|
+
this.ws?.send(JSON.stringify({ type: "get", payload: { key } }));
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
async set(key, value) {
|
|
517
|
+
if (!this.connected || !this.authenticated) throw new HumanEnvError("CLIENT_AUTH_INVALID_API_KEY" /* CLIENT_AUTH_INVALID_API_KEY */);
|
|
518
|
+
const msgId = `set-${Date.now()}`;
|
|
519
|
+
return new Promise((resolve, reject) => {
|
|
520
|
+
const timeout = setTimeout(() => {
|
|
521
|
+
this.pending.delete(msgId);
|
|
522
|
+
reject(new Error(`Timeout setting env: ${key}`));
|
|
523
|
+
}, 8e3);
|
|
524
|
+
this.pending.set(msgId, { resolve, reject, timeout });
|
|
525
|
+
this.ws?.send(JSON.stringify({ type: "set", payload: { key, value } }));
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
scheduleReconnect(reject) {
|
|
529
|
+
if (this.attempts >= this.config.maxRetries) {
|
|
530
|
+
reject(new HumanEnvError("CLIENT_CONN_MAX_RETRIES_EXCEEDED" /* CLIENT_CONN_MAX_RETRIES_EXCEEDED */));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.reconnecting = true;
|
|
534
|
+
this.attempts++;
|
|
535
|
+
const delay = Math.min(1e3 * Math.pow(2, this.attempts - 1), 3e4);
|
|
536
|
+
if (process.stdout.isTTY) {
|
|
537
|
+
console.error(`[humanenv] Reconnecting in ${delay}ms (attempt ${this.attempts}/${this.config.maxRetries})...`);
|
|
538
|
+
}
|
|
539
|
+
this.retryTimer = setTimeout(() => {
|
|
540
|
+
this.doConnect(() => {
|
|
541
|
+
}, reject);
|
|
542
|
+
}, delay);
|
|
543
|
+
}
|
|
544
|
+
startPing() {
|
|
545
|
+
this.stopPing();
|
|
546
|
+
this.pingTimer = setInterval(() => {
|
|
547
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
548
|
+
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
549
|
+
}
|
|
550
|
+
}, 3e4);
|
|
551
|
+
}
|
|
552
|
+
stopPing() {
|
|
553
|
+
if (this.pingTimer) {
|
|
554
|
+
clearInterval(this.pingTimer);
|
|
555
|
+
this.pingTimer = null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** Send a generate_api_key request to the server */
|
|
559
|
+
async generateApiKey() {
|
|
560
|
+
if (!this.connected || !this.authenticated) throw new Error("Client not authenticated");
|
|
561
|
+
return new Promise((resolve, reject) => {
|
|
562
|
+
const msgId = `genkey-${Date.now()}`;
|
|
563
|
+
const timeout = setTimeout(() => {
|
|
564
|
+
this.pending.delete(msgId);
|
|
565
|
+
reject(new Error("Timeout waiting for API key generation"));
|
|
566
|
+
}, 6e4);
|
|
567
|
+
this.pending.set(msgId, { resolve: (v) => resolve(v.apiKey), reject, timeout });
|
|
568
|
+
this.ws?.send(JSON.stringify({ type: "generate_api_key", payload: { projectName: this.config.projectName } }));
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/** Connect (creates fresh WS) and waits for auth response up to `timeoutMs`. Resolves silently on timeout. */
|
|
572
|
+
async connectAndWaitForAuth(timeoutMs) {
|
|
573
|
+
return new Promise((resolve) => {
|
|
574
|
+
if (this.connected && this.authenticated) {
|
|
575
|
+
resolve();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const deadline = Date.now() + timeoutMs;
|
|
579
|
+
const checkInterval = setInterval(() => {
|
|
580
|
+
if (this.connected && this.authenticated) {
|
|
581
|
+
clearInterval(checkInterval);
|
|
582
|
+
resolve();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (Date.now() >= deadline) {
|
|
586
|
+
clearInterval(checkInterval);
|
|
587
|
+
resolve();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
}, 200);
|
|
591
|
+
if (!this.connected) {
|
|
592
|
+
this.attempts = 0;
|
|
593
|
+
this.doConnect(() => {
|
|
594
|
+
}, () => {
|
|
595
|
+
clearInterval(checkInterval);
|
|
596
|
+
resolve();
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
disconnect() {
|
|
602
|
+
this.stopPing();
|
|
603
|
+
if (this.retryTimer) {
|
|
604
|
+
clearTimeout(this.retryTimer);
|
|
605
|
+
this.retryTimer = null;
|
|
606
|
+
}
|
|
607
|
+
this.disconnecting = true;
|
|
608
|
+
this.reconnecting = false;
|
|
609
|
+
this.ws?.close();
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// src/cli/entry.js
|
|
616
|
+
var { Command } = require("commander");
|
|
617
|
+
var fs = require("fs");
|
|
618
|
+
var path = require("path");
|
|
619
|
+
var os = require("os");
|
|
620
|
+
var { execSync } = require("child_process");
|
|
621
|
+
var { generateFingerprint: generateFingerprint2, SKILL_CONTENT: SKILL_CONTENT2 } = (init_shared(), __toCommonJS(shared_exports));
|
|
622
|
+
var { HumanEnvClient: HumanEnvClient2 } = (init_ws_manager(), __toCommonJS(ws_manager_exports));
|
|
623
|
+
var CREDENTIALS_DIR = path.join(os.homedir(), ".humanenv");
|
|
624
|
+
var isJson = process.argv.includes("--json") || process.argv.includes("-j");
|
|
625
|
+
var cleanArgs = process.argv.slice(2).filter((a) => a !== "--json" && a !== "-j");
|
|
626
|
+
var hasCmd = ["auth", "get", "set", "server"].some((c) => cleanArgs.includes(c));
|
|
627
|
+
var wantsHelp = cleanArgs.includes("--help") || cleanArgs.includes("-h");
|
|
628
|
+
if (wantsHelp && isJson) {
|
|
629
|
+
const commandName = cleanArgs.find((a) => ["auth", "get", "set", "server"].includes(a));
|
|
630
|
+
const commandsConfig = {
|
|
631
|
+
"auth": {
|
|
632
|
+
command: "auth",
|
|
633
|
+
description: "Authenticate with a HumanEnv server",
|
|
634
|
+
usage: "humanenv auth [options]",
|
|
635
|
+
options: [
|
|
636
|
+
{ flags: "--project-name <name>", description: "Project name", required: false, optional: true, shorthand: "pn" },
|
|
637
|
+
{ flags: "--pn <name>", description: "Project name (shorthand)", required: false, optional: true, shorthand: null },
|
|
638
|
+
{ flags: "--server-url <url>", description: "Server URL", required: false, optional: true, shorthand: "su" },
|
|
639
|
+
{ flags: "--su <url>", description: "Server URL (shorthand)", required: false, optional: true, shorthand: null },
|
|
640
|
+
{ flags: "--api-key <key>", description: "API key (optional)", required: false, optional: true, shorthand: null },
|
|
641
|
+
{ flags: "--generate-api-key", description: "Request a new API key from the server", required: false, optional: false, shorthand: null },
|
|
642
|
+
{ flags: "-h, --help", description: "Display help", required: false, optional: false, shorthand: "h" }
|
|
643
|
+
],
|
|
644
|
+
arguments: []
|
|
645
|
+
},
|
|
646
|
+
"get": {
|
|
647
|
+
command: "get",
|
|
648
|
+
description: "Retrieve an environment variable",
|
|
649
|
+
usage: "humanenv get <key>",
|
|
650
|
+
options: [
|
|
651
|
+
{ flags: "-h, --help", description: "Display help", required: false, optional: false, shorthand: "h" }
|
|
652
|
+
],
|
|
653
|
+
arguments: [{ name: "key", required: true, description: "Environment variable key" }]
|
|
654
|
+
},
|
|
655
|
+
"set": {
|
|
656
|
+
command: "set",
|
|
657
|
+
description: "Set an environment variable",
|
|
658
|
+
usage: "humanenv set <key> <value>",
|
|
659
|
+
options: [
|
|
660
|
+
{ flags: "-h, --help", description: "Display help", required: false, optional: false, shorthand: "h" }
|
|
661
|
+
],
|
|
662
|
+
arguments: [
|
|
663
|
+
{ name: "key", required: true, description: "Environment variable key" },
|
|
664
|
+
{ name: "value", required: true, description: "Environment variable value" }
|
|
665
|
+
]
|
|
666
|
+
},
|
|
667
|
+
"server": {
|
|
668
|
+
command: "server",
|
|
669
|
+
description: "Start the HumanEnv admin server",
|
|
670
|
+
usage: "humanenv server [options]",
|
|
671
|
+
options: [
|
|
672
|
+
{ flags: "--port <port>", description: "Port to listen on (default: 3056)", required: false, optional: true, shorthand: null },
|
|
673
|
+
{ flags: "--basicAuth", description: "Enable basic auth for admin UI", required: false, optional: false, shorthand: null },
|
|
674
|
+
{ flags: "-h, --help", description: "Display help", required: false, optional: false, shorthand: "h" }
|
|
675
|
+
],
|
|
676
|
+
arguments: []
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
if (commandName && commandsConfig[commandName]) {
|
|
680
|
+
console.log(JSON.stringify(commandsConfig[commandName], null, 2));
|
|
681
|
+
} else {
|
|
682
|
+
console.log(JSON.stringify({
|
|
683
|
+
command: "humanenv",
|
|
684
|
+
description: "Secure environment variable injection",
|
|
685
|
+
usage: "humanenv [command] [options]",
|
|
686
|
+
commands: ["auth", "get", "set", "server"],
|
|
687
|
+
options: [
|
|
688
|
+
{ flags: "--json", description: "Output in JSON format" },
|
|
689
|
+
{ flags: "-j", description: "Shorthand for --json" },
|
|
690
|
+
{ flags: "-h, --help", description: "Display help" }
|
|
691
|
+
]
|
|
692
|
+
}, null, 2));
|
|
693
|
+
}
|
|
694
|
+
process.exit(0);
|
|
695
|
+
}
|
|
696
|
+
function errJson(code, msg, hint, extras) {
|
|
697
|
+
return JSON.stringify(Object.assign(
|
|
698
|
+
{ success: false, code, error: msg },
|
|
699
|
+
hint ? { hint } : {},
|
|
700
|
+
extras || {}
|
|
701
|
+
), null, 2);
|
|
702
|
+
}
|
|
703
|
+
function failJson(code, msg, hint, extras) {
|
|
704
|
+
if (isJson) {
|
|
705
|
+
console.error(errJson(code, msg, hint, extras));
|
|
706
|
+
} else {
|
|
707
|
+
console.error("Error:", msg);
|
|
708
|
+
if (hint) console.error("Hint:", hint);
|
|
709
|
+
}
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
if (!hasCmd && !wantsHelp) {
|
|
713
|
+
ensureSkillFile();
|
|
714
|
+
const sp = path.join(process.cwd(), ".agents", "skills", "humanenv-usage", "SKILL.md");
|
|
715
|
+
const creds = readCredentials();
|
|
716
|
+
const fp = generateFingerprint2();
|
|
717
|
+
if (!process.stdout.isTTY || isJson) {
|
|
718
|
+
if (fs.existsSync(sp)) console.log(fs.readFileSync(sp, "utf8"));
|
|
719
|
+
console.log("");
|
|
720
|
+
if (creds.projectName && creds.serverUrl) {
|
|
721
|
+
console.error(JSON.stringify({
|
|
722
|
+
code: "AUTH_STATUS",
|
|
723
|
+
status: "credentials_found",
|
|
724
|
+
fingerprint: fp,
|
|
725
|
+
projectName: creds.projectName,
|
|
726
|
+
serverUrl: creds.serverUrl,
|
|
727
|
+
step: "Run humanenv auth to connect, then ask the admin to whitelist your fingerprint in the dashboard."
|
|
728
|
+
}));
|
|
729
|
+
} else {
|
|
730
|
+
console.error(JSON.stringify({
|
|
731
|
+
code: "NOT_AUTHENTICATED",
|
|
732
|
+
fingerprint: fp,
|
|
733
|
+
step: "Run: humanenv auth --project-name <name> --server-url <url> (shorthand: --pn / --su)"
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
console.log("HumanEnv - Secure environment variable injection");
|
|
738
|
+
console.log("");
|
|
739
|
+
console.log("Your fingerprint:", fp);
|
|
740
|
+
console.log("Share this with the project admin to whitelist.");
|
|
741
|
+
if (creds.serverUrl) console.log("Admin UI:", creds.serverUrl);
|
|
742
|
+
console.log("");
|
|
743
|
+
if (creds.projectName && creds.serverUrl) {
|
|
744
|
+
console.log("Credentials configured:");
|
|
745
|
+
console.log(" projectName:", creds.projectName);
|
|
746
|
+
console.log(" serverUrl:", creds.serverUrl);
|
|
747
|
+
console.log("");
|
|
748
|
+
console.log("Next steps:");
|
|
749
|
+
console.log(" 1. Run: humanenv auth");
|
|
750
|
+
console.log(" 2. Admin whitelists your fingerprint in the dashboard at", creds.serverUrl);
|
|
751
|
+
console.log(" 3. Start using: humanenv get <key>");
|
|
752
|
+
} else {
|
|
753
|
+
console.log("Usage:");
|
|
754
|
+
console.log(" humanenv auth --project-name <name> --server-url <url> [--api-key <key>]");
|
|
755
|
+
console.log(" humanenv get <key>");
|
|
756
|
+
console.log(" humanenv set <key> <value>");
|
|
757
|
+
console.log(" humanenv server [--port 3056] [--basicAuth]");
|
|
758
|
+
}
|
|
759
|
+
console.log("");
|
|
760
|
+
console.log("Aliases:");
|
|
761
|
+
console.log(" --pn short for --project-name");
|
|
762
|
+
console.log(" --su short for --server-url");
|
|
763
|
+
console.log(" --json structured JSON output (agent-friendly)");
|
|
764
|
+
console.log("");
|
|
765
|
+
}
|
|
766
|
+
process.exit(0);
|
|
767
|
+
}
|
|
768
|
+
function ensureSkillFile() {
|
|
769
|
+
const sp = path.join(process.cwd(), ".agents", "skills", "humanenv-usage", "SKILL.md");
|
|
770
|
+
if (!fs.existsSync(sp)) {
|
|
771
|
+
fs.mkdirSync(path.dirname(sp), { recursive: true });
|
|
772
|
+
fs.writeFileSync(sp, SKILL_CONTENT2, "utf8");
|
|
773
|
+
}
|
|
774
|
+
return sp;
|
|
775
|
+
}
|
|
776
|
+
function readCredentials() {
|
|
777
|
+
const p = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
778
|
+
if (!fs.existsSync(p)) return {};
|
|
779
|
+
try {
|
|
780
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
781
|
+
} catch {
|
|
782
|
+
return {};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function writeCredentials(d) {
|
|
786
|
+
if (!fs.existsSync(CREDENTIALS_DIR)) fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
787
|
+
fs.writeFileSync(path.join(CREDENTIALS_DIR, "credentials.json"), JSON.stringify(d, null, 2), "utf8");
|
|
788
|
+
}
|
|
789
|
+
function sleep(ms) {
|
|
790
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
791
|
+
}
|
|
792
|
+
var pName = (o) => o.projectName || o.pn;
|
|
793
|
+
var pUrl = (o) => o.serverUrl || o.su;
|
|
794
|
+
var nameOpt = ["--project-name <name>", "Project name"];
|
|
795
|
+
var urlOpt = ["--server-url <url>", "Server URL"];
|
|
796
|
+
var pnOpt = ["--pn <name>", "Project name (shorthand)"];
|
|
797
|
+
var suOpt = ["--su <url>", "Server URL (shorthand)"];
|
|
798
|
+
async function runAuth(opts) {
|
|
799
|
+
ensureSkillFile();
|
|
800
|
+
const projectName = pName(opts);
|
|
801
|
+
const serverUrl = pUrl(opts);
|
|
802
|
+
if (!projectName || !serverUrl) {
|
|
803
|
+
failJson(
|
|
804
|
+
"MISSING_ARGS",
|
|
805
|
+
"--project-name and --server-url are required",
|
|
806
|
+
"Run: humanenv auth --project-name <name> --server-url <url> (shorthand: --pn / --su)"
|
|
807
|
+
);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
writeCredentials({ projectName, serverUrl, apiKey: opts.apiKey || void 0 });
|
|
811
|
+
if (opts.generateApiKey) {
|
|
812
|
+
const client2 = new HumanEnvClient2({ serverUrl, projectName, projectApiKey: opts.apiKey || "", maxRetries: 1 });
|
|
813
|
+
try {
|
|
814
|
+
await client2.connect();
|
|
815
|
+
const key = await client2.generateApiKey();
|
|
816
|
+
if (isJson) console.error(errJson("API_KEY_GENERATED", "API key generated.", null, { success: true, apiKey: key }));
|
|
817
|
+
else console.log("API key generated:", key);
|
|
818
|
+
} catch (e) {
|
|
819
|
+
const fingerprint2 = generateFingerprint2();
|
|
820
|
+
failJson(
|
|
821
|
+
"GENERATE_API_KEY_FAILED",
|
|
822
|
+
`API key generation failed: ${e.message}`,
|
|
823
|
+
"Ensure the server is running, the project exists, and an admin is online to approve.",
|
|
824
|
+
{ fingerprint: fingerprint2, projectName, serverUrl }
|
|
825
|
+
);
|
|
826
|
+
} finally {
|
|
827
|
+
client2.disconnect();
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
let client = new HumanEnvClient2({ serverUrl, projectName, projectApiKey: opts.apiKey || "", maxRetries: 1 });
|
|
832
|
+
try {
|
|
833
|
+
await client.connect();
|
|
834
|
+
} catch (e) {
|
|
835
|
+
const fingerprint2 = generateFingerprint2();
|
|
836
|
+
let hint = "Re-run auth to retry.";
|
|
837
|
+
if (/ECONNREFUSED|ENOTFOUND/i.test(e.message)) {
|
|
838
|
+
hint = `Start the server first: humanenv server (or check --server-url)`;
|
|
839
|
+
} else if (/project.*not.*found/i.test(e.message)) {
|
|
840
|
+
hint = `Create the project first via the admin UI at ${serverUrl}`;
|
|
841
|
+
} else if (/api.*key.*invalid/i.test(e.message)) {
|
|
842
|
+
hint = "Re-run auth with --generate-api-key to request a new key from the admin.";
|
|
843
|
+
}
|
|
844
|
+
failJson(
|
|
845
|
+
"AUTH_FAILED",
|
|
846
|
+
`Auth failed: ${e.message}`,
|
|
847
|
+
hint,
|
|
848
|
+
{ fingerprint: fingerprint2, projectName, serverUrl }
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (client.whitelistStatus === "approved") {
|
|
852
|
+
if (isJson) {
|
|
853
|
+
console.error(errJson("AUTH_OK", "Authentication successful.", null, { success: true, whitelisted: true }));
|
|
854
|
+
} else {
|
|
855
|
+
console.log("Authentication successful.");
|
|
856
|
+
}
|
|
857
|
+
if (client) client.disconnect();
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const fingerprint = generateFingerprint2();
|
|
861
|
+
if (process.stdout.isTTY && !isJson) {
|
|
862
|
+
console.log("Auth OK, not whitelisted yet.");
|
|
863
|
+
console.log("Fingerprint:", fingerprint);
|
|
864
|
+
console.log("Waiting for admin approval... (120s timeout, Ctrl+C to abort)");
|
|
865
|
+
let attempts = 0;
|
|
866
|
+
while (attempts < 120) {
|
|
867
|
+
attempts++;
|
|
868
|
+
await sleep(1e3);
|
|
869
|
+
try {
|
|
870
|
+
if (client) client.disconnect();
|
|
871
|
+
await sleep(100);
|
|
872
|
+
client = new HumanEnvClient2({ serverUrl, projectName, projectApiKey: opts.apiKey || "", maxRetries: 1 });
|
|
873
|
+
await client.connect();
|
|
874
|
+
if (client.whitelistStatus === "approved") {
|
|
875
|
+
console.log("\nApproved. You can now use humanenv get/set.");
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
console.log("\nTimed out waiting for approval.");
|
|
882
|
+
console.log("To approve manually: open the admin UI at " + serverUrl);
|
|
883
|
+
console.log("Then go to the Whitelist tab and approve fingerprint:", fingerprint);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (isJson) {
|
|
887
|
+
console.error(errJson(
|
|
888
|
+
"AUTH_PENDING",
|
|
889
|
+
"Auth OK but not whitelisted yet.",
|
|
890
|
+
`Share this fingerprint with the admin to approve it.`,
|
|
891
|
+
{
|
|
892
|
+
success: true,
|
|
893
|
+
whitelisted: false,
|
|
894
|
+
fingerprint,
|
|
895
|
+
projectName,
|
|
896
|
+
serverUrl,
|
|
897
|
+
step: "admin_must_approve_whitelist"
|
|
898
|
+
}
|
|
899
|
+
));
|
|
900
|
+
} else {
|
|
901
|
+
console.log("Auth OK, not whitelisted yet.");
|
|
902
|
+
console.log("Fingerprint:", fingerprint);
|
|
903
|
+
console.log("Share this with the admin to approve it.");
|
|
904
|
+
}
|
|
905
|
+
if (client) client.disconnect();
|
|
906
|
+
}
|
|
907
|
+
function resolveCreds(opts) {
|
|
908
|
+
const st = readCredentials();
|
|
909
|
+
const projectName = pName(opts) || st.projectName;
|
|
910
|
+
const serverUrl = pUrl(opts) || st.serverUrl;
|
|
911
|
+
if (!projectName || !serverUrl) {
|
|
912
|
+
failJson(
|
|
913
|
+
"NOT_AUTHENTICATED",
|
|
914
|
+
"No credentials found.",
|
|
915
|
+
"Run: humanenv auth --project-name <name> --server-url <url> (or pass --pn / --su per-command)",
|
|
916
|
+
{ tip: "You can also set HUMANENV_PROJECT_NAME and HUMANENV_SERVER_URL env vars." }
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
projectName,
|
|
921
|
+
serverUrl,
|
|
922
|
+
apiKey: opts.apiKey || st.apiKey || "",
|
|
923
|
+
fingerprint: generateFingerprint2()
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
var program = new Command();
|
|
927
|
+
program.command("auth").description("Authenticate with a HumanEnv server").option(...nameOpt).option(...pnOpt).option(...urlOpt).option(...suOpt).option("--api-key <key>", "API key (optional)").option("--generate-api-key", "Request a new API key from the server").action(async (o) => {
|
|
928
|
+
await runAuth(o);
|
|
929
|
+
});
|
|
930
|
+
program.command("get").description("Retrieve an environment variable").argument("<key>", "Environment variable key").option(...nameOpt).option(...pnOpt).option(...urlOpt).option(...suOpt).action(async (key, opts) => {
|
|
931
|
+
const c = resolveCreds(opts);
|
|
932
|
+
const cli = new HumanEnvClient2({
|
|
933
|
+
serverUrl: c.serverUrl,
|
|
934
|
+
projectName: c.projectName,
|
|
935
|
+
projectApiKey: c.apiKey || "",
|
|
936
|
+
maxRetries: 3
|
|
937
|
+
});
|
|
938
|
+
try {
|
|
939
|
+
await cli.connect();
|
|
940
|
+
const value = await cli.get(key);
|
|
941
|
+
if (isJson) {
|
|
942
|
+
console.error(JSON.stringify({ success: true, code: "ENV_RETRIEVED", key, hasValue: !!value }, null, 2));
|
|
943
|
+
process.stdout.write(value);
|
|
944
|
+
} else if (!process.stdout.isTTY) {
|
|
945
|
+
process.stdout.write(value);
|
|
946
|
+
} else {
|
|
947
|
+
console.log(value);
|
|
948
|
+
}
|
|
949
|
+
} catch (e) {
|
|
950
|
+
const fingerprint = c.fingerprint;
|
|
951
|
+
let hint = null;
|
|
952
|
+
let code = "GET_FAILED";
|
|
953
|
+
if (/whitelist/i.test(e.message) || /not.*approved/i.test(e.message)) {
|
|
954
|
+
code = "CLIENT_NOT_WHITELISTED";
|
|
955
|
+
hint = `Run humanenv auth to submit your fingerprint. Then ask the admin to approve it at ${c.serverUrl}`;
|
|
956
|
+
} else if (/api.?mode/i.test(e.message)) {
|
|
957
|
+
code = "ENV_API_MODE_ONLY";
|
|
958
|
+
hint = "This env flag is API-mode only: use the SDK (await humanenv.get()) instead of CLI.";
|
|
959
|
+
} else if (/not.*found|key.*not.*exist/i.test(e.message)) {
|
|
960
|
+
code = "ENV_KEY_NOT_FOUND";
|
|
961
|
+
hint = `Key "${key}" does not exist. Create it via humanenv set or the admin UI.`;
|
|
962
|
+
} else if (/ECONNREFUSED|ENOTFOUND/i.test(e.message)) {
|
|
963
|
+
hint = `Start the server first: humanenv server (or verify ${c.serverUrl})`;
|
|
964
|
+
} else if (/timeout/i.test(e.message)) {
|
|
965
|
+
hint = "Server took too long to respond. Check network connectivity.";
|
|
966
|
+
} else if (/invalid.*api.*key/i.test(e.message)) {
|
|
967
|
+
hint = "Your API key may have expired. Run humanenv auth --generate-api-key for a new one.";
|
|
968
|
+
}
|
|
969
|
+
failJson(
|
|
970
|
+
code,
|
|
971
|
+
`Failed to get env: ${e.message}`,
|
|
972
|
+
hint,
|
|
973
|
+
{ key, fingerprint, projectName: c.projectName, serverUrl: c.serverUrl }
|
|
974
|
+
);
|
|
975
|
+
} finally {
|
|
976
|
+
cli.disconnect();
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
program.command("set").description("Set an environment variable").argument("<key>", "Environment variable key").argument("<value>", "Environment variable value").option(...nameOpt).option(...pnOpt).option(...urlOpt).option(...suOpt).action(async (key, value, opts) => {
|
|
980
|
+
const c = resolveCreds(opts);
|
|
981
|
+
const cli = new HumanEnvClient2({
|
|
982
|
+
serverUrl: c.serverUrl,
|
|
983
|
+
projectName: c.projectName,
|
|
984
|
+
projectApiKey: c.apiKey || "",
|
|
985
|
+
maxRetries: 3
|
|
986
|
+
});
|
|
987
|
+
try {
|
|
988
|
+
await cli.connect();
|
|
989
|
+
await cli.set(key, value);
|
|
990
|
+
if (isJson) {
|
|
991
|
+
console.error(JSON.stringify({ success: true, code: "ENV_SET", key }, null, 2));
|
|
992
|
+
} else {
|
|
993
|
+
console.log("Set", key);
|
|
994
|
+
}
|
|
995
|
+
} catch (e) {
|
|
996
|
+
const fingerprint = c.fingerprint;
|
|
997
|
+
let hint = null;
|
|
998
|
+
let code = "SET_FAILED";
|
|
999
|
+
if (/whitelist/i.test(e.message) || /not.*approved/i.test(e.message)) {
|
|
1000
|
+
code = "CLIENT_NOT_WHITELISTED";
|
|
1001
|
+
hint = `Run humanenv auth to submit your fingerprint. Then ask the admin to approve it at ${c.serverUrl}`;
|
|
1002
|
+
} else if (/ECONNREFUSED|ENOTFOUND/i.test(e.message)) {
|
|
1003
|
+
hint = `Start the server first: humanenv server (or verify ${c.serverUrl})`;
|
|
1004
|
+
} else if (/timeout/i.test(e.message)) {
|
|
1005
|
+
hint = "Server took too long to respond. Check network connectivity.";
|
|
1006
|
+
}
|
|
1007
|
+
failJson(
|
|
1008
|
+
code,
|
|
1009
|
+
`Failed to set env: ${e.message}`,
|
|
1010
|
+
hint,
|
|
1011
|
+
{ key, fingerprint, projectName: c.projectName, serverUrl: c.serverUrl }
|
|
1012
|
+
);
|
|
1013
|
+
} finally {
|
|
1014
|
+
cli.disconnect();
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
program.command("server").description("Start the HumanEnv admin server").option("--port <port>", "Port to listen on (default: 3056)").option("--basicAuth", "Enable basic auth for admin UI").action((opts) => {
|
|
1018
|
+
const args = [];
|
|
1019
|
+
if (opts.port) args.push("--port", opts.port);
|
|
1020
|
+
if (opts.basicAuth) args.push("--basicAuth");
|
|
1021
|
+
try {
|
|
1022
|
+
execSync(
|
|
1023
|
+
`npx tsx ${path.join(__dirname, "..", "..", "server", "src", "index.ts")} ${args.join(" ")}`,
|
|
1024
|
+
{ stdio: "inherit" }
|
|
1025
|
+
);
|
|
1026
|
+
} catch {
|
|
1027
|
+
try {
|
|
1028
|
+
execSync(
|
|
1029
|
+
`node ${path.join(__dirname, "..", "..", "server", "dist", "index.js")} ${args.join(" ")}`,
|
|
1030
|
+
{ stdio: "inherit" }
|
|
1031
|
+
);
|
|
1032
|
+
} catch {
|
|
1033
|
+
failJson(
|
|
1034
|
+
"SERVER_NOT_FOUND",
|
|
1035
|
+
"Server binary not found.",
|
|
1036
|
+
"Install humanenv-server or run from the monorepo."
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
program.parse(["node", "humanenv", ...cleanArgs]);
|