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,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
HumanEnvClient: () => HumanEnvClient,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/shared/errors.ts
|
|
39
|
+
var ErrorMessages = {
|
|
40
|
+
SERVER_PK_NOT_AVAILABLE: "Server private key is not available. Restart pending.",
|
|
41
|
+
CLIENT_AUTH_INVALID_PROJECT_NAME: "Invalid or unknown project name.",
|
|
42
|
+
CLIENT_AUTH_NOT_WHITELISTED: "Client fingerprint is not whitelisted for this project.",
|
|
43
|
+
CLIENT_AUTH_INVALID_API_KEY: "Invalid or expired API key.",
|
|
44
|
+
CLIENT_CONN_MAX_RETRIES_EXCEEDED: "Maximum WS connection retries exceeded.",
|
|
45
|
+
ENV_API_MODE_ONLY: "This env is API-mode only and cannot be accessed via CLI.",
|
|
46
|
+
SERVER_INTERNAL_ERROR: "An internal server error occurred.",
|
|
47
|
+
WS_CONNECTION_FAILED: "Failed to establish WebSocket connection.",
|
|
48
|
+
DB_OPERATION_FAILED: "Database operation failed."
|
|
49
|
+
};
|
|
50
|
+
var HumanEnvError = class extends Error {
|
|
51
|
+
code;
|
|
52
|
+
constructor(code, message) {
|
|
53
|
+
super(message ?? ErrorMessages[code]);
|
|
54
|
+
this.name = "HumanEnvError";
|
|
55
|
+
this.code = code;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/shared/crypto.ts
|
|
60
|
+
var crypto = __toESM(require("node:crypto"));
|
|
61
|
+
function generateFingerprint() {
|
|
62
|
+
const components = [
|
|
63
|
+
process.env.HOSTNAME || "unknown-host",
|
|
64
|
+
process.platform,
|
|
65
|
+
process.arch,
|
|
66
|
+
process.version
|
|
67
|
+
];
|
|
68
|
+
return crypto.createHash("sha256").update(components.join("|")).digest("hex").slice(0, 16);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/ws-manager.ts
|
|
72
|
+
var import_ws = __toESM(require("ws"));
|
|
73
|
+
var HumanEnvClient = class {
|
|
74
|
+
ws = null;
|
|
75
|
+
connected = false;
|
|
76
|
+
authenticated = false;
|
|
77
|
+
_whitelistStatus = null;
|
|
78
|
+
attempts = 0;
|
|
79
|
+
pending = /* @__PURE__ */ new Map();
|
|
80
|
+
config;
|
|
81
|
+
retryTimer = null;
|
|
82
|
+
pingTimer = null;
|
|
83
|
+
reconnecting = false;
|
|
84
|
+
disconnecting = false;
|
|
85
|
+
_authResolve = null;
|
|
86
|
+
_authReject = null;
|
|
87
|
+
get whitelistStatus() {
|
|
88
|
+
return this._whitelistStatus;
|
|
89
|
+
}
|
|
90
|
+
constructor(config) {
|
|
91
|
+
this.config = {
|
|
92
|
+
serverUrl: config.serverUrl,
|
|
93
|
+
projectName: config.projectName,
|
|
94
|
+
projectApiKey: config.projectApiKey || "",
|
|
95
|
+
maxRetries: config.maxRetries ?? 10
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
getFingerprint() {
|
|
99
|
+
return generateFingerprint();
|
|
100
|
+
}
|
|
101
|
+
async connect() {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
this.doConnect(resolve, reject);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
doConnect(resolve, reject) {
|
|
107
|
+
const proto = this.config.serverUrl.startsWith("https") ? "wss" : "ws";
|
|
108
|
+
const host = this.config.serverUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
109
|
+
const url = `${proto}://${host}/ws`;
|
|
110
|
+
this.ws = new import_ws.default(url);
|
|
111
|
+
this.ws.on("open", () => {
|
|
112
|
+
this.connected = true;
|
|
113
|
+
this.attempts = 0;
|
|
114
|
+
this.reconnecting = false;
|
|
115
|
+
this.startPing();
|
|
116
|
+
this.sendAuth(resolve, reject);
|
|
117
|
+
});
|
|
118
|
+
this.ws.on("message", (raw) => {
|
|
119
|
+
try {
|
|
120
|
+
const msg = JSON.parse(raw.toString());
|
|
121
|
+
this.handleMessage(msg);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
this.ws.on("close", () => {
|
|
126
|
+
this.connected = false;
|
|
127
|
+
this.authenticated = false;
|
|
128
|
+
this.stopPing();
|
|
129
|
+
if (!this.disconnecting && !this.reconnecting) this.scheduleReconnect(reject);
|
|
130
|
+
});
|
|
131
|
+
this.ws.on("error", () => {
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
sendAuth(resolve, reject) {
|
|
135
|
+
this._authResolve = resolve;
|
|
136
|
+
this._authReject = reject;
|
|
137
|
+
this.ws?.send(JSON.stringify({
|
|
138
|
+
type: "auth",
|
|
139
|
+
payload: {
|
|
140
|
+
projectName: this.config.projectName,
|
|
141
|
+
apiKey: this.config.projectApiKey,
|
|
142
|
+
fingerprint: this.getFingerprint()
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
handleMessage(msg) {
|
|
147
|
+
if (msg.type === "auth_response") {
|
|
148
|
+
if (msg.payload.success) {
|
|
149
|
+
this.authenticated = true;
|
|
150
|
+
this._whitelistStatus = msg.payload.status || (msg.payload.whitelisted ? "approved" : "pending");
|
|
151
|
+
this._authResolve?.();
|
|
152
|
+
} else {
|
|
153
|
+
this._authReject?.(new HumanEnvError(msg.payload.code, msg.payload.error));
|
|
154
|
+
}
|
|
155
|
+
this._authResolve = null;
|
|
156
|
+
this._authReject = null;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (msg.type === "get_response") {
|
|
160
|
+
this._resolvePending("get", msg.payload);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (msg.type === "set_response") {
|
|
164
|
+
this._resolvePending("set", msg.payload);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (msg.type === "pong") {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
_resolvePending(kind, payload) {
|
|
171
|
+
for (const [id, op] of this.pending) {
|
|
172
|
+
clearTimeout(op.timeout);
|
|
173
|
+
this.pending.delete(id);
|
|
174
|
+
if (payload.error) {
|
|
175
|
+
op.reject(new HumanEnvError(payload.code, payload.error));
|
|
176
|
+
} else {
|
|
177
|
+
op.resolve(payload);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async get(keyOrKeys) {
|
|
183
|
+
if (!this.connected || !this.authenticated) throw new HumanEnvError("CLIENT_AUTH_INVALID_API_KEY" /* CLIENT_AUTH_INVALID_API_KEY */);
|
|
184
|
+
if (Array.isArray(keyOrKeys)) {
|
|
185
|
+
const result = {};
|
|
186
|
+
await Promise.all(keyOrKeys.map(async (key) => {
|
|
187
|
+
result[key] = await this._getSingle(key);
|
|
188
|
+
}));
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
return this._getSingle(keyOrKeys);
|
|
192
|
+
}
|
|
193
|
+
_getSingle(key) {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const msgId = `${key}-${Date.now()}`;
|
|
196
|
+
const timeout = setTimeout(() => {
|
|
197
|
+
this.pending.delete(msgId);
|
|
198
|
+
reject(new Error(`Timeout getting env: ${key}`));
|
|
199
|
+
}, 8e3);
|
|
200
|
+
this.pending.set(msgId, { resolve: (v) => resolve(v.value), reject, timeout });
|
|
201
|
+
this.ws?.send(JSON.stringify({ type: "get", payload: { key } }));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async set(key, value) {
|
|
205
|
+
if (!this.connected || !this.authenticated) throw new HumanEnvError("CLIENT_AUTH_INVALID_API_KEY" /* CLIENT_AUTH_INVALID_API_KEY */);
|
|
206
|
+
const msgId = `set-${Date.now()}`;
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const timeout = setTimeout(() => {
|
|
209
|
+
this.pending.delete(msgId);
|
|
210
|
+
reject(new Error(`Timeout setting env: ${key}`));
|
|
211
|
+
}, 8e3);
|
|
212
|
+
this.pending.set(msgId, { resolve, reject, timeout });
|
|
213
|
+
this.ws?.send(JSON.stringify({ type: "set", payload: { key, value } }));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
scheduleReconnect(reject) {
|
|
217
|
+
if (this.attempts >= this.config.maxRetries) {
|
|
218
|
+
reject(new HumanEnvError("CLIENT_CONN_MAX_RETRIES_EXCEEDED" /* CLIENT_CONN_MAX_RETRIES_EXCEEDED */));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.reconnecting = true;
|
|
222
|
+
this.attempts++;
|
|
223
|
+
const delay = Math.min(1e3 * Math.pow(2, this.attempts - 1), 3e4);
|
|
224
|
+
if (process.stdout.isTTY) {
|
|
225
|
+
console.error(`[humanenv] Reconnecting in ${delay}ms (attempt ${this.attempts}/${this.config.maxRetries})...`);
|
|
226
|
+
}
|
|
227
|
+
this.retryTimer = setTimeout(() => {
|
|
228
|
+
this.doConnect(() => {
|
|
229
|
+
}, reject);
|
|
230
|
+
}, delay);
|
|
231
|
+
}
|
|
232
|
+
startPing() {
|
|
233
|
+
this.stopPing();
|
|
234
|
+
this.pingTimer = setInterval(() => {
|
|
235
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
236
|
+
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
237
|
+
}
|
|
238
|
+
}, 3e4);
|
|
239
|
+
}
|
|
240
|
+
stopPing() {
|
|
241
|
+
if (this.pingTimer) {
|
|
242
|
+
clearInterval(this.pingTimer);
|
|
243
|
+
this.pingTimer = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Send a generate_api_key request to the server */
|
|
247
|
+
async generateApiKey() {
|
|
248
|
+
if (!this.connected || !this.authenticated) throw new Error("Client not authenticated");
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
const msgId = `genkey-${Date.now()}`;
|
|
251
|
+
const timeout = setTimeout(() => {
|
|
252
|
+
this.pending.delete(msgId);
|
|
253
|
+
reject(new Error("Timeout waiting for API key generation"));
|
|
254
|
+
}, 6e4);
|
|
255
|
+
this.pending.set(msgId, { resolve: (v) => resolve(v.apiKey), reject, timeout });
|
|
256
|
+
this.ws?.send(JSON.stringify({ type: "generate_api_key", payload: { projectName: this.config.projectName } }));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** Connect (creates fresh WS) and waits for auth response up to `timeoutMs`. Resolves silently on timeout. */
|
|
260
|
+
async connectAndWaitForAuth(timeoutMs) {
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
if (this.connected && this.authenticated) {
|
|
263
|
+
resolve();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const deadline = Date.now() + timeoutMs;
|
|
267
|
+
const checkInterval = setInterval(() => {
|
|
268
|
+
if (this.connected && this.authenticated) {
|
|
269
|
+
clearInterval(checkInterval);
|
|
270
|
+
resolve();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (Date.now() >= deadline) {
|
|
274
|
+
clearInterval(checkInterval);
|
|
275
|
+
resolve();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}, 200);
|
|
279
|
+
if (!this.connected) {
|
|
280
|
+
this.attempts = 0;
|
|
281
|
+
this.doConnect(() => {
|
|
282
|
+
}, () => {
|
|
283
|
+
clearInterval(checkInterval);
|
|
284
|
+
resolve();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
disconnect() {
|
|
290
|
+
this.stopPing();
|
|
291
|
+
if (this.retryTimer) {
|
|
292
|
+
clearTimeout(this.retryTimer);
|
|
293
|
+
this.retryTimer = null;
|
|
294
|
+
}
|
|
295
|
+
this.disconnecting = true;
|
|
296
|
+
this.reconnecting = false;
|
|
297
|
+
this.ws?.close();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/index.ts
|
|
302
|
+
var singleton = null;
|
|
303
|
+
var configSet = false;
|
|
304
|
+
async function ensure() {
|
|
305
|
+
if (!singleton) throw new Error("humanenv.config() must be called first");
|
|
306
|
+
return singleton;
|
|
307
|
+
}
|
|
308
|
+
var index_default = {
|
|
309
|
+
config(cfg) {
|
|
310
|
+
if (configSet) return;
|
|
311
|
+
configSet = true;
|
|
312
|
+
singleton = new HumanEnvClient(cfg);
|
|
313
|
+
},
|
|
314
|
+
async get(keyOrKeys) {
|
|
315
|
+
const client = await ensure();
|
|
316
|
+
return client.get(keyOrKeys);
|
|
317
|
+
},
|
|
318
|
+
async set(key, value) {
|
|
319
|
+
const client = await ensure();
|
|
320
|
+
return client.set(key, value);
|
|
321
|
+
},
|
|
322
|
+
disconnect() {
|
|
323
|
+
if (singleton) {
|
|
324
|
+
singleton.disconnect();
|
|
325
|
+
singleton = null;
|
|
326
|
+
configSet = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
331
|
+
0 && (module.exports = {
|
|
332
|
+
HumanEnvClient
|
|
333
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// src/shared/errors.ts
|
|
2
|
+
var ErrorMessages = {
|
|
3
|
+
SERVER_PK_NOT_AVAILABLE: "Server private key is not available. Restart pending.",
|
|
4
|
+
CLIENT_AUTH_INVALID_PROJECT_NAME: "Invalid or unknown project name.",
|
|
5
|
+
CLIENT_AUTH_NOT_WHITELISTED: "Client fingerprint is not whitelisted for this project.",
|
|
6
|
+
CLIENT_AUTH_INVALID_API_KEY: "Invalid or expired API key.",
|
|
7
|
+
CLIENT_CONN_MAX_RETRIES_EXCEEDED: "Maximum WS connection retries exceeded.",
|
|
8
|
+
ENV_API_MODE_ONLY: "This env is API-mode only and cannot be accessed via CLI.",
|
|
9
|
+
SERVER_INTERNAL_ERROR: "An internal server error occurred.",
|
|
10
|
+
WS_CONNECTION_FAILED: "Failed to establish WebSocket connection.",
|
|
11
|
+
DB_OPERATION_FAILED: "Database operation failed."
|
|
12
|
+
};
|
|
13
|
+
var HumanEnvError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(code, message) {
|
|
16
|
+
super(message ?? ErrorMessages[code]);
|
|
17
|
+
this.name = "HumanEnvError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/shared/crypto.ts
|
|
23
|
+
import * as crypto from "node:crypto";
|
|
24
|
+
function generateFingerprint() {
|
|
25
|
+
const components = [
|
|
26
|
+
process.env.HOSTNAME || "unknown-host",
|
|
27
|
+
process.platform,
|
|
28
|
+
process.arch,
|
|
29
|
+
process.version
|
|
30
|
+
];
|
|
31
|
+
return crypto.createHash("sha256").update(components.join("|")).digest("hex").slice(0, 16);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/ws-manager.ts
|
|
35
|
+
import WebSocket from "ws";
|
|
36
|
+
var HumanEnvClient = class {
|
|
37
|
+
ws = null;
|
|
38
|
+
connected = false;
|
|
39
|
+
authenticated = false;
|
|
40
|
+
_whitelistStatus = null;
|
|
41
|
+
attempts = 0;
|
|
42
|
+
pending = /* @__PURE__ */ new Map();
|
|
43
|
+
config;
|
|
44
|
+
retryTimer = null;
|
|
45
|
+
pingTimer = null;
|
|
46
|
+
reconnecting = false;
|
|
47
|
+
disconnecting = false;
|
|
48
|
+
_authResolve = null;
|
|
49
|
+
_authReject = null;
|
|
50
|
+
get whitelistStatus() {
|
|
51
|
+
return this._whitelistStatus;
|
|
52
|
+
}
|
|
53
|
+
constructor(config) {
|
|
54
|
+
this.config = {
|
|
55
|
+
serverUrl: config.serverUrl,
|
|
56
|
+
projectName: config.projectName,
|
|
57
|
+
projectApiKey: config.projectApiKey || "",
|
|
58
|
+
maxRetries: config.maxRetries ?? 10
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
getFingerprint() {
|
|
62
|
+
return generateFingerprint();
|
|
63
|
+
}
|
|
64
|
+
async connect() {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
this.doConnect(resolve, reject);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
doConnect(resolve, reject) {
|
|
70
|
+
const proto = this.config.serverUrl.startsWith("https") ? "wss" : "ws";
|
|
71
|
+
const host = this.config.serverUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
72
|
+
const url = `${proto}://${host}/ws`;
|
|
73
|
+
this.ws = new WebSocket(url);
|
|
74
|
+
this.ws.on("open", () => {
|
|
75
|
+
this.connected = true;
|
|
76
|
+
this.attempts = 0;
|
|
77
|
+
this.reconnecting = false;
|
|
78
|
+
this.startPing();
|
|
79
|
+
this.sendAuth(resolve, reject);
|
|
80
|
+
});
|
|
81
|
+
this.ws.on("message", (raw) => {
|
|
82
|
+
try {
|
|
83
|
+
const msg = JSON.parse(raw.toString());
|
|
84
|
+
this.handleMessage(msg);
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.ws.on("close", () => {
|
|
89
|
+
this.connected = false;
|
|
90
|
+
this.authenticated = false;
|
|
91
|
+
this.stopPing();
|
|
92
|
+
if (!this.disconnecting && !this.reconnecting) this.scheduleReconnect(reject);
|
|
93
|
+
});
|
|
94
|
+
this.ws.on("error", () => {
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
sendAuth(resolve, reject) {
|
|
98
|
+
this._authResolve = resolve;
|
|
99
|
+
this._authReject = reject;
|
|
100
|
+
this.ws?.send(JSON.stringify({
|
|
101
|
+
type: "auth",
|
|
102
|
+
payload: {
|
|
103
|
+
projectName: this.config.projectName,
|
|
104
|
+
apiKey: this.config.projectApiKey,
|
|
105
|
+
fingerprint: this.getFingerprint()
|
|
106
|
+
}
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
handleMessage(msg) {
|
|
110
|
+
if (msg.type === "auth_response") {
|
|
111
|
+
if (msg.payload.success) {
|
|
112
|
+
this.authenticated = true;
|
|
113
|
+
this._whitelistStatus = msg.payload.status || (msg.payload.whitelisted ? "approved" : "pending");
|
|
114
|
+
this._authResolve?.();
|
|
115
|
+
} else {
|
|
116
|
+
this._authReject?.(new HumanEnvError(msg.payload.code, msg.payload.error));
|
|
117
|
+
}
|
|
118
|
+
this._authResolve = null;
|
|
119
|
+
this._authReject = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (msg.type === "get_response") {
|
|
123
|
+
this._resolvePending("get", msg.payload);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (msg.type === "set_response") {
|
|
127
|
+
this._resolvePending("set", msg.payload);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (msg.type === "pong") {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
_resolvePending(kind, payload) {
|
|
134
|
+
for (const [id, op] of this.pending) {
|
|
135
|
+
clearTimeout(op.timeout);
|
|
136
|
+
this.pending.delete(id);
|
|
137
|
+
if (payload.error) {
|
|
138
|
+
op.reject(new HumanEnvError(payload.code, payload.error));
|
|
139
|
+
} else {
|
|
140
|
+
op.resolve(payload);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async get(keyOrKeys) {
|
|
146
|
+
if (!this.connected || !this.authenticated) throw new HumanEnvError("CLIENT_AUTH_INVALID_API_KEY" /* CLIENT_AUTH_INVALID_API_KEY */);
|
|
147
|
+
if (Array.isArray(keyOrKeys)) {
|
|
148
|
+
const result = {};
|
|
149
|
+
await Promise.all(keyOrKeys.map(async (key) => {
|
|
150
|
+
result[key] = await this._getSingle(key);
|
|
151
|
+
}));
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
return this._getSingle(keyOrKeys);
|
|
155
|
+
}
|
|
156
|
+
_getSingle(key) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const msgId = `${key}-${Date.now()}`;
|
|
159
|
+
const timeout = setTimeout(() => {
|
|
160
|
+
this.pending.delete(msgId);
|
|
161
|
+
reject(new Error(`Timeout getting env: ${key}`));
|
|
162
|
+
}, 8e3);
|
|
163
|
+
this.pending.set(msgId, { resolve: (v) => resolve(v.value), reject, timeout });
|
|
164
|
+
this.ws?.send(JSON.stringify({ type: "get", payload: { key } }));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async set(key, value) {
|
|
168
|
+
if (!this.connected || !this.authenticated) throw new HumanEnvError("CLIENT_AUTH_INVALID_API_KEY" /* CLIENT_AUTH_INVALID_API_KEY */);
|
|
169
|
+
const msgId = `set-${Date.now()}`;
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const timeout = setTimeout(() => {
|
|
172
|
+
this.pending.delete(msgId);
|
|
173
|
+
reject(new Error(`Timeout setting env: ${key}`));
|
|
174
|
+
}, 8e3);
|
|
175
|
+
this.pending.set(msgId, { resolve, reject, timeout });
|
|
176
|
+
this.ws?.send(JSON.stringify({ type: "set", payload: { key, value } }));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
scheduleReconnect(reject) {
|
|
180
|
+
if (this.attempts >= this.config.maxRetries) {
|
|
181
|
+
reject(new HumanEnvError("CLIENT_CONN_MAX_RETRIES_EXCEEDED" /* CLIENT_CONN_MAX_RETRIES_EXCEEDED */));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.reconnecting = true;
|
|
185
|
+
this.attempts++;
|
|
186
|
+
const delay = Math.min(1e3 * Math.pow(2, this.attempts - 1), 3e4);
|
|
187
|
+
if (process.stdout.isTTY) {
|
|
188
|
+
console.error(`[humanenv] Reconnecting in ${delay}ms (attempt ${this.attempts}/${this.config.maxRetries})...`);
|
|
189
|
+
}
|
|
190
|
+
this.retryTimer = setTimeout(() => {
|
|
191
|
+
this.doConnect(() => {
|
|
192
|
+
}, reject);
|
|
193
|
+
}, delay);
|
|
194
|
+
}
|
|
195
|
+
startPing() {
|
|
196
|
+
this.stopPing();
|
|
197
|
+
this.pingTimer = setInterval(() => {
|
|
198
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
199
|
+
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
200
|
+
}
|
|
201
|
+
}, 3e4);
|
|
202
|
+
}
|
|
203
|
+
stopPing() {
|
|
204
|
+
if (this.pingTimer) {
|
|
205
|
+
clearInterval(this.pingTimer);
|
|
206
|
+
this.pingTimer = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Send a generate_api_key request to the server */
|
|
210
|
+
async generateApiKey() {
|
|
211
|
+
if (!this.connected || !this.authenticated) throw new Error("Client not authenticated");
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const msgId = `genkey-${Date.now()}`;
|
|
214
|
+
const timeout = setTimeout(() => {
|
|
215
|
+
this.pending.delete(msgId);
|
|
216
|
+
reject(new Error("Timeout waiting for API key generation"));
|
|
217
|
+
}, 6e4);
|
|
218
|
+
this.pending.set(msgId, { resolve: (v) => resolve(v.apiKey), reject, timeout });
|
|
219
|
+
this.ws?.send(JSON.stringify({ type: "generate_api_key", payload: { projectName: this.config.projectName } }));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/** Connect (creates fresh WS) and waits for auth response up to `timeoutMs`. Resolves silently on timeout. */
|
|
223
|
+
async connectAndWaitForAuth(timeoutMs) {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
if (this.connected && this.authenticated) {
|
|
226
|
+
resolve();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const deadline = Date.now() + timeoutMs;
|
|
230
|
+
const checkInterval = setInterval(() => {
|
|
231
|
+
if (this.connected && this.authenticated) {
|
|
232
|
+
clearInterval(checkInterval);
|
|
233
|
+
resolve();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (Date.now() >= deadline) {
|
|
237
|
+
clearInterval(checkInterval);
|
|
238
|
+
resolve();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}, 200);
|
|
242
|
+
if (!this.connected) {
|
|
243
|
+
this.attempts = 0;
|
|
244
|
+
this.doConnect(() => {
|
|
245
|
+
}, () => {
|
|
246
|
+
clearInterval(checkInterval);
|
|
247
|
+
resolve();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
disconnect() {
|
|
253
|
+
this.stopPing();
|
|
254
|
+
if (this.retryTimer) {
|
|
255
|
+
clearTimeout(this.retryTimer);
|
|
256
|
+
this.retryTimer = null;
|
|
257
|
+
}
|
|
258
|
+
this.disconnecting = true;
|
|
259
|
+
this.reconnecting = false;
|
|
260
|
+
this.ws?.close();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/index.ts
|
|
265
|
+
var singleton = null;
|
|
266
|
+
var configSet = false;
|
|
267
|
+
async function ensure() {
|
|
268
|
+
if (!singleton) throw new Error("humanenv.config() must be called first");
|
|
269
|
+
return singleton;
|
|
270
|
+
}
|
|
271
|
+
var index_default = {
|
|
272
|
+
config(cfg) {
|
|
273
|
+
if (configSet) return;
|
|
274
|
+
configSet = true;
|
|
275
|
+
singleton = new HumanEnvClient(cfg);
|
|
276
|
+
},
|
|
277
|
+
async get(keyOrKeys) {
|
|
278
|
+
const client = await ensure();
|
|
279
|
+
return client.get(keyOrKeys);
|
|
280
|
+
},
|
|
281
|
+
async set(key, value) {
|
|
282
|
+
const client = await ensure();
|
|
283
|
+
return client.set(key, value);
|
|
284
|
+
},
|
|
285
|
+
disconnect() {
|
|
286
|
+
if (singleton) {
|
|
287
|
+
singleton.disconnect();
|
|
288
|
+
singleton = null;
|
|
289
|
+
configSet = false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
export {
|
|
294
|
+
HumanEnvClient,
|
|
295
|
+
index_default as default
|
|
296
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "humanenv",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Securely inject environment variables into your app. Secrets for humans only.",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"humanenv": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"author": "arancibiajav@gmail.com",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git@github.com:javimosch/humanenv.git"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node build.js",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^12.1.0",
|
|
21
|
+
"ws": "^8.18.0"
|
|
22
|
+
},
|
|
23
|
+
"files": ["src/", "dist/"]
|
|
24
|
+
}
|