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.
Files changed (36) hide show
  1. package/README.md +303 -0
  2. package/package.json +38 -0
  3. package/packages/cli/package.json +15 -0
  4. package/packages/cli/src/bin.js +228 -0
  5. package/packages/client/bin.js +174 -0
  6. package/packages/client/build.js +57 -0
  7. package/packages/client/dist/cli.js +1041 -0
  8. package/packages/client/dist/index.cjs +333 -0
  9. package/packages/client/dist/index.mjs +296 -0
  10. package/packages/client/package.json +24 -0
  11. package/packages/client/src/cli/bin.js +228 -0
  12. package/packages/client/src/cli/entry.js +465 -0
  13. package/packages/client/src/index.ts +31 -0
  14. package/packages/client/src/shared/buffer-shim.d.ts +4 -0
  15. package/packages/client/src/shared/crypto.ts +98 -0
  16. package/packages/client/src/shared/errors.ts +32 -0
  17. package/packages/client/src/shared/index.ts +3 -0
  18. package/packages/client/src/shared/types.ts +118 -0
  19. package/packages/client/src/ws-manager.ts +263 -0
  20. package/packages/server/package.json +21 -0
  21. package/packages/server/src/auth.ts +13 -0
  22. package/packages/server/src/db/index.ts +19 -0
  23. package/packages/server/src/db/interface.ts +33 -0
  24. package/packages/server/src/db/mongo.ts +166 -0
  25. package/packages/server/src/db/sqlite.ts +180 -0
  26. package/packages/server/src/index.ts +123 -0
  27. package/packages/server/src/pk-manager.ts +79 -0
  28. package/packages/server/src/routes/index.ts +110 -0
  29. package/packages/server/src/views/index.ejs +359 -0
  30. package/packages/server/src/ws/router.ts +263 -0
  31. package/packages/shared/package.json +13 -0
  32. package/packages/shared/src/buffer-shim.d.ts +4 -0
  33. package/packages/shared/src/crypto.ts +98 -0
  34. package/packages/shared/src/errors.ts +32 -0
  35. package/packages/shared/src/index.ts +3 -0
  36. package/packages/shared/src/types.ts +119 -0
@@ -0,0 +1,98 @@
1
+ import * as crypto from 'node:crypto'
2
+
3
+ const PBKDF2_ITERATIONS = 100_000
4
+ const PK_KEY_LENGTH = 32
5
+
6
+ // ============================================================
7
+ // Mnemonic helpers (BIP39-compatible wordlist handling)
8
+ // ============================================================
9
+
10
+ const BIP39_WORDLIST = [
11
+ 'abandon', 'ability', 'able', 'about', 'above', 'absent', 'absorb', 'abstract',
12
+ 'absurd', 'abuse', 'access', 'accident', 'account', 'accuse', 'achieve', 'acid',
13
+ 'acoustic', 'acquire', 'across', 'act', 'action', 'actor', 'actress', 'actual',
14
+ 'adapt', 'add', 'addict', 'address', 'adjust', 'admit', 'adult', 'advance',
15
+ 'advice', 'aerobic', 'affair', 'afford', 'afraid', 'again', 'age', 'agent',
16
+ 'agree', 'ahead', 'aim', 'air', 'airport', 'aisle', 'alarm', 'album',
17
+ 'alcohol', 'alert', 'alien', 'all', 'alley', 'allow', 'almost', 'alone',
18
+ 'alpha', 'already', 'also', 'alter', 'always', 'amateur', 'amazing', 'among',
19
+ 'amount', 'amused', 'analyst', 'anchor', 'ancient', 'anger', 'angle', 'angry',
20
+ 'animal', 'ankle', 'announce', 'annual', 'another', 'answer', 'antenna',
21
+ 'antique', 'anxiety', 'any', 'apart', 'apology', 'appear', 'apple', 'approve',
22
+ 'april', 'arch', 'arctic', 'area', 'arena', 'argue', 'arm', 'armed', 'armor',
23
+ 'army', 'around', 'arrest', 'arrive', 'arrow', 'art', 'artist', 'artwork',
24
+ 'ask', 'aspect', 'assault', 'asset', 'assist', 'assume', 'asthma', 'athlete',
25
+ 'atom', 'attack', 'attend', 'attitude', 'attract', 'auction', 'audit', 'august',
26
+ 'aunt', 'author', 'auto', 'autumn', 'average', 'avocado', 'avoid', 'awake',
27
+ 'aware', 'awesome', 'awful', 'awkward', 'axis', 'baby', 'bachelor', 'bacon',
28
+ 'badge', 'bag', 'balance', 'balcony', 'ball', 'bamboo', 'banana', 'banner',
29
+ 'bar', 'barely', 'bargain', 'barrel', 'base', 'basic', 'basket', 'battle',
30
+ 'beach', 'bean', 'beauty', 'because', 'become', 'beef', 'before', 'begin',
31
+ 'behave', 'behind', 'believe', 'below', 'bench', 'benefit', 'best', 'betray',
32
+ 'better', 'between', 'beyond', 'bicycle', 'bid', 'bike', 'bind', 'biology',
33
+ 'bird', 'birth', 'bitter', 'black', 'blade', 'blame', 'blanket', 'blast'
34
+ ]
35
+
36
+ export function generateMnemonic(): string {
37
+ const entropy = crypto.randomBytes(16)
38
+ const words: string[] = []
39
+ for (let i = 0; i < 32; i++) {
40
+ words.push(BIP39_WORDLIST[entropy[i]! % BIP39_WORDLIST.length])
41
+ }
42
+ return words.slice(0, 12).join(' ')
43
+ }
44
+
45
+ export function validateMnemonic(mnemonic: string): boolean {
46
+ const words = mnemonic.trim().toLowerCase().split(/\s+/)
47
+ if (words.length !== 12) return false
48
+ return words.every(w => BIP39_WORDLIST.includes(w))
49
+ }
50
+
51
+ export function derivePkFromMnemonic(mnemonic: string): Buffer {
52
+ return crypto.pbkdf2Sync(
53
+ mnemonic.toLowerCase().trim(),
54
+ 'humanenv-server-v1',
55
+ PBKDF2_ITERATIONS,
56
+ PK_KEY_LENGTH,
57
+ 'sha256'
58
+ ) as any
59
+ }
60
+
61
+ export function hashPkForVerification(pk: Buffer): string {
62
+ return crypto.createHash('sha256').update(pk as any).digest('hex')
63
+ }
64
+
65
+ export function encryptWithPk(value: string, pk: Buffer, aad: string): string {
66
+ const iv = crypto.randomBytes(12)
67
+ const cipher = crypto.createCipheriv('aes-256-gcm', pk as any, iv as any)
68
+ cipher.setAAD(crypto.createHash('sha256').update(aad).digest() as any)
69
+ const encrypted = Buffer.concat([cipher.update(value, 'utf8') as any, cipher.final() as any])
70
+ const tag = cipher.getAuthTag()
71
+ return Buffer.concat([iv as any, tag as any, encrypted as any]).toString('base64')
72
+ }
73
+
74
+ export function decryptWithPk(encryptedBase64: string, pk: Buffer, aad: string): string {
75
+ const buf = Buffer.from(encryptedBase64, 'base64')
76
+ const iv = buf.subarray(0, 12) as any
77
+ const tag = buf.subarray(12, 28) as any
78
+ const ciphertext = buf.subarray(28) as any
79
+ const decipher = crypto.createDecipheriv('aes-256-gcm', pk as any, iv as any)
80
+ decipher.setAAD(crypto.createHash('sha256').update(aad).digest() as any)
81
+ decipher.setAuthTag(tag)
82
+ const decrypted = Buffer.concat([decipher.update(ciphertext) as any, decipher.final() as any])
83
+ return decrypted.toString('utf8')
84
+ }
85
+
86
+ export function generateFingerprint(): string {
87
+ const components = [
88
+ process.env.HOSTNAME || 'unknown-host',
89
+ process.platform,
90
+ process.arch,
91
+ process.version,
92
+ ]
93
+ return crypto
94
+ .createHash('sha256')
95
+ .update(components.join('|'))
96
+ .digest('hex')
97
+ .slice(0, 16)
98
+ }
@@ -0,0 +1,32 @@
1
+ export enum ErrorCode {
2
+ SERVER_PK_NOT_AVAILABLE = 'SERVER_PK_NOT_AVAILABLE',
3
+ CLIENT_AUTH_INVALID_PROJECT_NAME = 'CLIENT_AUTH_INVALID_PROJECT_NAME',
4
+ CLIENT_AUTH_NOT_WHITELISTED = 'CLIENT_AUTH_NOT_WHITELISTED',
5
+ CLIENT_AUTH_INVALID_API_KEY = 'CLIENT_AUTH_INVALID_API_KEY',
6
+ CLIENT_CONN_MAX_RETRIES_EXCEEDED = 'CLIENT_CONN_MAX_RETRIES_EXCEEDED',
7
+ ENV_API_MODE_ONLY = 'ENV_API_MODE_ONLY',
8
+ SERVER_INTERNAL_ERROR = 'SERVER_INTERNAL_ERROR',
9
+ WS_CONNECTION_FAILED = 'WS_CONNECTION_FAILED',
10
+ DB_OPERATION_FAILED = 'DB_OPERATION_FAILED',
11
+ }
12
+
13
+ export const ErrorMessages: Record<ErrorCode, string> = {
14
+ SERVER_PK_NOT_AVAILABLE: 'Server private key is not available. Restart pending.',
15
+ CLIENT_AUTH_INVALID_PROJECT_NAME: 'Invalid or unknown project name.',
16
+ CLIENT_AUTH_NOT_WHITELISTED: 'Client fingerprint is not whitelisted for this project.',
17
+ CLIENT_AUTH_INVALID_API_KEY: 'Invalid or expired API key.',
18
+ CLIENT_CONN_MAX_RETRIES_EXCEEDED: 'Maximum WS connection retries exceeded.',
19
+ ENV_API_MODE_ONLY: 'This env is API-mode only and cannot be accessed via CLI.',
20
+ SERVER_INTERNAL_ERROR: 'An internal server error occurred.',
21
+ WS_CONNECTION_FAILED: 'Failed to establish WebSocket connection.',
22
+ DB_OPERATION_FAILED: 'Database operation failed.',
23
+ }
24
+
25
+ export class HumanEnvError extends Error {
26
+ public readonly code: ErrorCode
27
+ constructor(code: ErrorCode, message?: string) {
28
+ super(message ?? ErrorMessages[code])
29
+ this.name = 'HumanEnvError'
30
+ this.code = code
31
+ }
32
+ }
@@ -0,0 +1,3 @@
1
+ export * from './errors'
2
+ export * from './types'
3
+ export * from './crypto'
@@ -0,0 +1,119 @@
1
+ // ============================================================
2
+ // Domain models
3
+ // ============================================================
4
+
5
+ export interface Project {
6
+ id: string
7
+ name: string
8
+ createdAt: number
9
+ }
10
+
11
+ export interface Env {
12
+ id: string
13
+ projectId: string
14
+ key: string
15
+ encryptedValue: string
16
+ apiModeOnly: boolean
17
+ createdAt: number
18
+ }
19
+
20
+ export interface ApiKey {
21
+ id: string
22
+ projectId: string
23
+ encryptedValue: string
24
+ ttl?: number
25
+ expiresAt?: number
26
+ createdAt: number
27
+ }
28
+
29
+ export interface WhitelistEntry {
30
+ id: string
31
+ projectId: string
32
+ fingerprint: string
33
+ status: 'pending' | 'approved' | 'rejected'
34
+ createdAt: number
35
+ }
36
+
37
+ // ============================================================
38
+ // WebSocket message types
39
+ // ============================================================
40
+
41
+ export type WsMessage =
42
+ | { type: 'auth'; payload: AuthPayload }
43
+ | { type: 'auth_response'; payload: AuthResponse }
44
+ | { type: 'get'; payload: { key: string } }
45
+ | { type: 'get_response'; payload: { key: string; value: string } | { error: string; code: string } }
46
+ | { type: 'set'; payload: { key: string; value: string } }
47
+ | { type: 'set_response'; payload: { success: boolean } | { error: string; code: string } }
48
+ | { type: 'generate_api_key'; payload: { projectName: string } }
49
+ | { type: 'apikey_request'; payload: { clientFingerprint: string; projectName: string } }
50
+ | { type: 'apikey_response'; payload: { success: boolean; apiKey?: string } | { error: string; code: string } }
51
+ | { type: 'whitelist_request'; payload: { fingerprint: string; projectName: string } }
52
+ | { type: 'whitelist_response'; payload: { fingerprint: string; approved: boolean } }
53
+ | { type: 'disconnect'; payload?: never }
54
+ | { type: 'ping'; payload?: never }
55
+ | { type: 'pong'; payload?: never }
56
+
57
+ export interface AuthPayload {
58
+ projectName: string
59
+ apiKey: string
60
+ fingerprint: string
61
+ }
62
+
63
+ export interface AuthResponse {
64
+ success: boolean
65
+ whitelisted: boolean
66
+ status?: 'approved' | 'pending' | 'rejected'
67
+ error?: string
68
+ code?: string
69
+ }
70
+
71
+ // ============================================================
72
+ // Client library config
73
+ // ============================================================
74
+
75
+ export interface ClientConfig {
76
+ serverUrl: string
77
+ projectName: string
78
+ projectApiKey?: string
79
+ maxRetries?: number
80
+ }
81
+
82
+ export interface ServerConfig {
83
+ port: number
84
+ basicAuth?: { username: string; password: string }
85
+ dataDir: string
86
+ dbUri?: string
87
+ }
88
+
89
+ export type CredentialStore = {
90
+ projectName: string
91
+ serverUrl: string
92
+ apiKey?: string
93
+ }
94
+
95
+ export interface SkillMetadata {
96
+ name: string
97
+ description: string
98
+ category?: string
99
+ }
100
+
101
+ export const SKILL_CONTENT = `---
102
+ name: humanenv-usage
103
+ description: Use humanenv to retrieve sensitives/envs. Never log/read sensitives from humanenv. They are envs for humans only.
104
+ ---
105
+
106
+ ## How to retrieve sensitives
107
+
108
+ ### JavaScript API
109
+ Use \`humanenv.get('key')\` inside your application. The returned value must be used immediately and then set to \`null\` to prevent memory leaks.
110
+
111
+ ### CLI (non-TTY mode)
112
+ Only non-API-mode envs are accessible. Do not attempt to use this to log or dump all envs.
113
+
114
+ ## Rules
115
+ 1. NEVER log env values retrieved from humanenv
116
+ 2. NEVER dump or export multiple values at once
117
+ 3. ALWAYS null variables after use
118
+ 4. NEVER write sensitives to files
119
+ 5. Do not generate scripts that use humanenv in loops or to exfiltrate data`