0nmcp 1.7.0 → 2.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 +113 -48
- package/cli.js +705 -1
- package/command-runner.js +224 -0
- package/commands.js +115 -0
- package/index.js +15 -1
- package/lib/stats.json +1 -1
- package/package.json +42 -3
- package/vault/container.js +479 -0
- package/vault/crypto-container.js +278 -0
- package/vault/deed-collector.js +286 -0
- package/vault/deed-importer.js +277 -0
- package/vault/deed.js +319 -0
- package/vault/escrow.js +227 -0
- package/vault/layers.js +254 -0
- package/vault/registry.js +159 -0
- package/vault/seal.js +74 -0
- package/vault/tools-container.js +356 -0
- package/vault/tools-deed.js +257 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Vault: Container Crypto Primitives
|
|
3
|
+
// ============================================================
|
|
4
|
+
// AES-256-GCM encryption, Argon2id key derivation, X25519 ECDH,
|
|
5
|
+
// Ed25519 signing, SHA3-256 hashing for the 0nVault container.
|
|
6
|
+
//
|
|
7
|
+
// Patent Pending: US Provisional Patent Application #63/990,046
|
|
8
|
+
// "System and Method for Semantically-Layered Encrypted Digital
|
|
9
|
+
// Business Asset Transfer with Multi-Party Escrow Key Distribution
|
|
10
|
+
// and Public-Verifiable State Certification"
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
import { randomBytes, createCipheriv, createDecipheriv, randomUUID, pbkdf2Sync } from "crypto";
|
|
14
|
+
import { createRequire } from "module";
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
// ── Load optional dependencies ───────────────────────────────
|
|
19
|
+
let naclModule = null;
|
|
20
|
+
let sha3Module = null;
|
|
21
|
+
|
|
22
|
+
try { naclModule = require("tweetnacl"); } catch {}
|
|
23
|
+
try { sha3Module = require("js-sha3"); } catch {}
|
|
24
|
+
|
|
25
|
+
function requireNacl() {
|
|
26
|
+
if (naclModule) return naclModule;
|
|
27
|
+
throw new Error("tweetnacl package required. Install: npm i tweetnacl");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireSha3() {
|
|
31
|
+
if (sha3Module) return sha3Module;
|
|
32
|
+
throw new Error("js-sha3 package required. Install: npm i js-sha3");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Constants ────────────────────────────────────────────────
|
|
36
|
+
const ALGORITHM = "aes-256-gcm";
|
|
37
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
38
|
+
const IV_LENGTH = 12; // 96 bits (GCM recommended)
|
|
39
|
+
const SALT_LENGTH = 32; // 256 bits
|
|
40
|
+
const TAG_LENGTH = 16; // 128 bits (GCM auth tag)
|
|
41
|
+
|
|
42
|
+
// Argon2id parameters for credentials double-encryption
|
|
43
|
+
const ARGON2_MEMORY = 65536; // 64MB
|
|
44
|
+
const ARGON2_ITERATIONS = 4;
|
|
45
|
+
const ARGON2_PARALLELISM = 2;
|
|
46
|
+
const ARGON2_HASH_LENGTH = 32; // 256 bits
|
|
47
|
+
|
|
48
|
+
// ── Key Derivation ───────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derive key from passphrase using PBKDF2-SHA512.
|
|
52
|
+
*/
|
|
53
|
+
function deriveKeyPBKDF2(passphrase, salt) {
|
|
54
|
+
return pbkdf2Sync(passphrase, salt, 100000, KEY_LENGTH, "sha512");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derive key using Argon2id for credentials double-encryption.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} passphrase
|
|
61
|
+
* @param {Buffer} salt
|
|
62
|
+
* @returns {Promise<Buffer>} 32-byte key
|
|
63
|
+
*/
|
|
64
|
+
export async function deriveKeyArgon2(passphrase, salt) {
|
|
65
|
+
let argon2;
|
|
66
|
+
try { argon2 = await import("argon2"); } catch {
|
|
67
|
+
throw new Error("argon2 package required for credentials layer. Install: npm i argon2");
|
|
68
|
+
}
|
|
69
|
+
const mod = argon2.default || argon2;
|
|
70
|
+
const hash = await mod.hash(passphrase, {
|
|
71
|
+
type: 2, // argon2id
|
|
72
|
+
memoryCost: ARGON2_MEMORY,
|
|
73
|
+
timeCost: ARGON2_ITERATIONS,
|
|
74
|
+
parallelism: ARGON2_PARALLELISM,
|
|
75
|
+
hashLength: ARGON2_HASH_LENGTH,
|
|
76
|
+
salt,
|
|
77
|
+
raw: true,
|
|
78
|
+
});
|
|
79
|
+
return hash;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── AES-256-GCM ──────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Encrypt data with AES-256-GCM using a passphrase-derived key.
|
|
86
|
+
* Each call generates a unique IV and salt.
|
|
87
|
+
*
|
|
88
|
+
* @param {Buffer|string} plaintext - Data to encrypt
|
|
89
|
+
* @param {string} passphrase - Encryption passphrase
|
|
90
|
+
* @returns {{ ciphertext: Buffer, iv: Buffer, salt: Buffer, tag: Buffer }}
|
|
91
|
+
*/
|
|
92
|
+
export function encryptAES(plaintext, passphrase) {
|
|
93
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
94
|
+
const iv = randomBytes(IV_LENGTH);
|
|
95
|
+
const key = deriveKeyPBKDF2(passphrase, salt);
|
|
96
|
+
|
|
97
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
98
|
+
const data = typeof plaintext === "string" ? Buffer.from(plaintext, "utf8") : plaintext;
|
|
99
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
100
|
+
const tag = cipher.getAuthTag();
|
|
101
|
+
|
|
102
|
+
return { ciphertext: encrypted, iv, salt, tag };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Decrypt AES-256-GCM encrypted data.
|
|
107
|
+
*
|
|
108
|
+
* @param {Buffer} ciphertext - Encrypted data
|
|
109
|
+
* @param {string} passphrase - Decryption passphrase
|
|
110
|
+
* @param {Buffer} iv - Initialization vector
|
|
111
|
+
* @param {Buffer} salt - PBKDF2 salt
|
|
112
|
+
* @param {Buffer} tag - GCM auth tag
|
|
113
|
+
* @returns {Buffer} Decrypted data
|
|
114
|
+
*/
|
|
115
|
+
export function decryptAES(ciphertext, passphrase, iv, salt, tag) {
|
|
116
|
+
const key = deriveKeyPBKDF2(passphrase, salt);
|
|
117
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
118
|
+
decipher.setAuthTag(tag);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error("Decryption failed — wrong passphrase or corrupted data");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Encrypt data with a raw 256-bit key (for escrow layer keys).
|
|
129
|
+
*
|
|
130
|
+
* @param {Buffer} plaintext - Data to encrypt
|
|
131
|
+
* @param {Buffer} key - 32-byte AES key
|
|
132
|
+
* @returns {{ ciphertext: Buffer, iv: Buffer, tag: Buffer }}
|
|
133
|
+
*/
|
|
134
|
+
export function encryptAESRaw(plaintext, key) {
|
|
135
|
+
const iv = randomBytes(IV_LENGTH);
|
|
136
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
137
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
138
|
+
const tag = cipher.getAuthTag();
|
|
139
|
+
return { ciphertext: encrypted, iv, tag };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Decrypt with a raw 256-bit key.
|
|
144
|
+
*
|
|
145
|
+
* @param {Buffer} ciphertext
|
|
146
|
+
* @param {Buffer} key - 32-byte AES key
|
|
147
|
+
* @param {Buffer} iv
|
|
148
|
+
* @param {Buffer} tag
|
|
149
|
+
* @returns {Buffer}
|
|
150
|
+
*/
|
|
151
|
+
export function decryptAESRaw(ciphertext, key, iv, tag) {
|
|
152
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
153
|
+
decipher.setAuthTag(tag);
|
|
154
|
+
try {
|
|
155
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error("Decryption failed — wrong key or corrupted data");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Ed25519 Signing ──────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generate Ed25519 signing keypair.
|
|
165
|
+
* @returns {{ publicKey: Buffer, secretKey: Buffer }}
|
|
166
|
+
*/
|
|
167
|
+
export function generateSigningKeyPair() {
|
|
168
|
+
const nacl = requireNacl();
|
|
169
|
+
const pair = nacl.sign.keyPair();
|
|
170
|
+
return {
|
|
171
|
+
publicKey: Buffer.from(pair.publicKey),
|
|
172
|
+
secretKey: Buffer.from(pair.secretKey),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Sign data with Ed25519.
|
|
178
|
+
* @param {Buffer} data - Data to sign
|
|
179
|
+
* @param {Buffer} secretKey - 64-byte Ed25519 secret key
|
|
180
|
+
* @returns {Buffer} 64-byte signature
|
|
181
|
+
*/
|
|
182
|
+
export function sign(data, secretKey) {
|
|
183
|
+
const nacl = requireNacl();
|
|
184
|
+
const sig = nacl.sign.detached(new Uint8Array(data), new Uint8Array(secretKey));
|
|
185
|
+
return Buffer.from(sig);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Verify Ed25519 signature.
|
|
190
|
+
* @param {Buffer} data
|
|
191
|
+
* @param {Buffer} signature - 64-byte signature
|
|
192
|
+
* @param {Buffer} publicKey - 32-byte public key
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
export function verifySignature(data, signature, publicKey) {
|
|
196
|
+
const nacl = requireNacl();
|
|
197
|
+
return nacl.sign.detached.verify(
|
|
198
|
+
new Uint8Array(data),
|
|
199
|
+
new Uint8Array(signature),
|
|
200
|
+
new Uint8Array(publicKey)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── X25519 ECDH ──────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate X25519 keypair for escrow key exchange.
|
|
208
|
+
* @returns {{ publicKey: Buffer, secretKey: Buffer }}
|
|
209
|
+
*/
|
|
210
|
+
export function generateEscrowKeyPair() {
|
|
211
|
+
const nacl = requireNacl();
|
|
212
|
+
const pair = nacl.box.keyPair();
|
|
213
|
+
return {
|
|
214
|
+
publicKey: Buffer.from(pair.publicKey),
|
|
215
|
+
secretKey: Buffer.from(pair.secretKey),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Compute shared secret via X25519 ECDH.
|
|
221
|
+
* @param {Buffer} mySecretKey - 32-byte secret key
|
|
222
|
+
* @param {Buffer} theirPublicKey - 32-byte public key
|
|
223
|
+
* @returns {Buffer} 32-byte shared secret
|
|
224
|
+
*/
|
|
225
|
+
export function computeSharedSecret(mySecretKey, theirPublicKey) {
|
|
226
|
+
const nacl = requireNacl();
|
|
227
|
+
const shared = nacl.box.before(
|
|
228
|
+
new Uint8Array(theirPublicKey),
|
|
229
|
+
new Uint8Array(mySecretKey)
|
|
230
|
+
);
|
|
231
|
+
return Buffer.from(shared);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── SHA3-256 ─────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Compute SHA3-256 hash.
|
|
238
|
+
* @param {Buffer|string} data
|
|
239
|
+
* @returns {Buffer} 32-byte hash
|
|
240
|
+
*/
|
|
241
|
+
export function sha3(data) {
|
|
242
|
+
const sha3Lib = requireSha3();
|
|
243
|
+
const input = typeof data === "string" ? data : new Uint8Array(data);
|
|
244
|
+
const hash = sha3Lib.sha3_256.create().update(input).digest();
|
|
245
|
+
return Buffer.from(hash);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Compute SHA3-256 hash and return hex string.
|
|
250
|
+
* @param {Buffer|string} data
|
|
251
|
+
* @returns {string} 64-char hex hash
|
|
252
|
+
*/
|
|
253
|
+
export function sha3Hex(data) {
|
|
254
|
+
return sha3(data).toString("hex");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── UUID ─────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Generate a UUID v4 transfer ID.
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
export function generateTransferId() {
|
|
264
|
+
return randomUUID();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Exports ──────────────────────────────────────────────────
|
|
268
|
+
export {
|
|
269
|
+
ALGORITHM,
|
|
270
|
+
KEY_LENGTH,
|
|
271
|
+
IV_LENGTH,
|
|
272
|
+
SALT_LENGTH,
|
|
273
|
+
TAG_LENGTH,
|
|
274
|
+
ARGON2_MEMORY,
|
|
275
|
+
ARGON2_ITERATIONS,
|
|
276
|
+
ARGON2_PARALLELISM,
|
|
277
|
+
ARGON2_HASH_LENGTH,
|
|
278
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Vault: Deed Credential Collector
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Collects credentials from multiple sources (files, dirs,
|
|
5
|
+
// manual entry) and auto-detects services using the engine
|
|
6
|
+
// mapper. Feeds into BusinessDeed.create().
|
|
7
|
+
//
|
|
8
|
+
// Patent Pending: US Provisional Patent Application #63/990,046
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
12
|
+
import { join, extname } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { parseFile, parseEnvString, parseJsonString, parseCsvString } from "../engine/parser.js";
|
|
15
|
+
import { mapEnvVars, groupByService } from "../engine/mapper.js";
|
|
16
|
+
import { verifyCredentials } from "../engine/validator.js";
|
|
17
|
+
|
|
18
|
+
const CONNECTIONS_DIR = join(homedir(), ".0n", "connections");
|
|
19
|
+
|
|
20
|
+
// ── Collect from .0n connection files ───────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load credentials from ~/.0n/connections/*.0n files.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} [dir] - Connections directory (default: ~/.0n/connections/)
|
|
26
|
+
* @returns {{ credentials: Object, services: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function collectFromConnections(dir = CONNECTIONS_DIR) {
|
|
29
|
+
const credentials = {};
|
|
30
|
+
const services = [];
|
|
31
|
+
|
|
32
|
+
if (!existsSync(dir)) return { credentials, services };
|
|
33
|
+
|
|
34
|
+
const files = readdirSync(dir).filter(f => f.endsWith(".0n"));
|
|
35
|
+
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(join(dir, file), "utf-8");
|
|
39
|
+
const data = JSON.parse(content);
|
|
40
|
+
|
|
41
|
+
// .0n connection file format: { $0n: { type, service }, credentials: { ... } }
|
|
42
|
+
const service = data?.$0n?.service || file.replace(".0n", "");
|
|
43
|
+
if (data.credentials && typeof data.credentials === "object") {
|
|
44
|
+
credentials[service] = data.credentials;
|
|
45
|
+
services.push(service);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Skip invalid files
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { credentials, services };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Collect from file (auto-detect format) ──────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collect credentials from a file (.env, .json, .csv).
|
|
59
|
+
*
|
|
60
|
+
* @param {string} filePath
|
|
61
|
+
* @returns {{ credentials: Object, envVars: Object, services: string[], unmapped: Array }}
|
|
62
|
+
*/
|
|
63
|
+
export function collectFromFile(filePath) {
|
|
64
|
+
const { entries } = parseFile(filePath);
|
|
65
|
+
return processEntries(entries);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Collect from string content ─────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Collect from raw .env string content.
|
|
72
|
+
* @param {string} content
|
|
73
|
+
* @returns {{ credentials: Object, envVars: Object, services: string[], unmapped: Array }}
|
|
74
|
+
*/
|
|
75
|
+
export function collectFromEnvString(content) {
|
|
76
|
+
const entries = parseEnvString(content);
|
|
77
|
+
return processEntries(entries);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Collect from raw JSON string content.
|
|
82
|
+
* @param {string} content
|
|
83
|
+
* @returns {{ credentials: Object, envVars: Object, services: string[], unmapped: Array }}
|
|
84
|
+
*/
|
|
85
|
+
export function collectFromJsonString(content) {
|
|
86
|
+
const entries = parseJsonString(content);
|
|
87
|
+
return processEntries(entries);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Collect from raw CSV string content.
|
|
92
|
+
* @param {string} content
|
|
93
|
+
* @returns {{ credentials: Object, envVars: Object, services: string[], unmapped: Array }}
|
|
94
|
+
*/
|
|
95
|
+
export function collectFromCsvString(content) {
|
|
96
|
+
const entries = parseCsvString(content);
|
|
97
|
+
return processEntries(entries);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Collect from manual key-value pairs ─────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Collect from manual key-value pairs.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array<{ key: string, value: string }>} pairs
|
|
106
|
+
* @returns {{ credentials: Object, envVars: Object, services: string[], unmapped: Array }}
|
|
107
|
+
*/
|
|
108
|
+
export function collectFromManual(pairs) {
|
|
109
|
+
return processEntries(pairs);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Collect from pre-structured service credentials ─────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Collect from already-structured credentials object.
|
|
116
|
+
* Format: { stripe: { apiKey: "sk_..." }, github: { token: "ghp_..." } }
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} structured
|
|
119
|
+
* @returns {{ credentials: Object, services: string[] }}
|
|
120
|
+
*/
|
|
121
|
+
export function collectFromStructured(structured) {
|
|
122
|
+
const services = Object.keys(structured);
|
|
123
|
+
return { credentials: structured, services };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Process entries through mapper ──────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Process raw key-value entries through the mapper pipeline.
|
|
130
|
+
*
|
|
131
|
+
* @param {Array<{ key: string, value: string }>} entries
|
|
132
|
+
* @returns {{ credentials: Object, envVars: Object, services: string[], unmapped: Array }}
|
|
133
|
+
*/
|
|
134
|
+
function processEntries(entries) {
|
|
135
|
+
const { mapped, unmapped } = mapEnvVars(entries);
|
|
136
|
+
const grouped = groupByService(mapped);
|
|
137
|
+
|
|
138
|
+
// Build credentials and envVars
|
|
139
|
+
const credentials = {};
|
|
140
|
+
const envVars = {};
|
|
141
|
+
const services = [];
|
|
142
|
+
|
|
143
|
+
for (const [service, group] of Object.entries(grouped)) {
|
|
144
|
+
credentials[service] = group.credentials;
|
|
145
|
+
services.push(service);
|
|
146
|
+
for (const envVar of group.envVars) {
|
|
147
|
+
const entry = entries.find(e => e.key === envVar);
|
|
148
|
+
if (entry) envVars[entry.key] = entry.value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Unmapped entries go to envVars
|
|
153
|
+
for (const entry of unmapped) {
|
|
154
|
+
envVars[entry.key] = entry.value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { credentials, envVars, services, unmapped };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Validate credentials live ───────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate collected credentials against live API endpoints.
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} credentials - { service: { field: value } }
|
|
166
|
+
* @returns {Promise<Object>} Validation results per service
|
|
167
|
+
*/
|
|
168
|
+
export async function validateCollected(credentials) {
|
|
169
|
+
const results = {};
|
|
170
|
+
const summary = { total: 0, valid: 0, invalid: 0, skipped: 0 };
|
|
171
|
+
|
|
172
|
+
const entries = Object.entries(credentials);
|
|
173
|
+
summary.total = entries.length;
|
|
174
|
+
|
|
175
|
+
// Validate in parallel batches of 5
|
|
176
|
+
const batchSize = 5;
|
|
177
|
+
for (let i = 0; i < entries.length; i += batchSize) {
|
|
178
|
+
const batch = entries.slice(i, i + batchSize);
|
|
179
|
+
const promises = batch.map(([service, creds]) =>
|
|
180
|
+
verifyCredentials(service, creds).then(r => ({ service, ...r }))
|
|
181
|
+
);
|
|
182
|
+
const batchResults = await Promise.allSettled(promises);
|
|
183
|
+
|
|
184
|
+
for (const result of batchResults) {
|
|
185
|
+
if (result.status === "fulfilled") {
|
|
186
|
+
const r = result.value;
|
|
187
|
+
results[r.service] = r;
|
|
188
|
+
if (r.skipped) summary.skipped++;
|
|
189
|
+
else if (r.valid) summary.valid++;
|
|
190
|
+
else summary.invalid++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { results, summary };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Master collection function ──────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Collect credentials from multiple sources at once.
|
|
202
|
+
*
|
|
203
|
+
* @param {Object} sources
|
|
204
|
+
* @param {string} [sources.envFile] - Path to .env file
|
|
205
|
+
* @param {string} [sources.jsonFile] - Path to JSON file
|
|
206
|
+
* @param {string} [sources.csvFile] - Path to CSV file
|
|
207
|
+
* @param {string} [sources.connectionsDir] - Path to connections dir
|
|
208
|
+
* @param {Array} [sources.manual] - Manual key-value pairs
|
|
209
|
+
* @param {Object} [sources.structured] - Pre-structured credentials
|
|
210
|
+
* @param {boolean} [sources.validate] - Validate credentials live
|
|
211
|
+
* @returns {Promise<Object>}
|
|
212
|
+
*/
|
|
213
|
+
export async function collectCredentials(sources) {
|
|
214
|
+
const allCredentials = {};
|
|
215
|
+
const allEnvVars = {};
|
|
216
|
+
const allServices = new Set();
|
|
217
|
+
const allUnmapped = [];
|
|
218
|
+
|
|
219
|
+
// Collect from each source
|
|
220
|
+
if (sources.envFile && existsSync(sources.envFile)) {
|
|
221
|
+
const result = collectFromFile(sources.envFile);
|
|
222
|
+
mergeResults(allCredentials, allEnvVars, allServices, allUnmapped, result);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (sources.jsonFile && existsSync(sources.jsonFile)) {
|
|
226
|
+
const result = collectFromFile(sources.jsonFile);
|
|
227
|
+
mergeResults(allCredentials, allEnvVars, allServices, allUnmapped, result);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (sources.csvFile && existsSync(sources.csvFile)) {
|
|
231
|
+
const result = collectFromFile(sources.csvFile);
|
|
232
|
+
mergeResults(allCredentials, allEnvVars, allServices, allUnmapped, result);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (sources.connectionsDir || existsSync(CONNECTIONS_DIR)) {
|
|
236
|
+
const dir = sources.connectionsDir || CONNECTIONS_DIR;
|
|
237
|
+
const result = collectFromConnections(dir);
|
|
238
|
+
for (const [svc, creds] of Object.entries(result.credentials)) {
|
|
239
|
+
allCredentials[svc] = { ...allCredentials[svc], ...creds };
|
|
240
|
+
allServices.add(svc);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (sources.manual && Array.isArray(sources.manual)) {
|
|
245
|
+
const result = collectFromManual(sources.manual);
|
|
246
|
+
mergeResults(allCredentials, allEnvVars, allServices, allUnmapped, result);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (sources.structured && typeof sources.structured === "object") {
|
|
250
|
+
const result = collectFromStructured(sources.structured);
|
|
251
|
+
for (const [svc, creds] of Object.entries(result.credentials)) {
|
|
252
|
+
allCredentials[svc] = { ...allCredentials[svc], ...creds };
|
|
253
|
+
allServices.add(svc);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const output = {
|
|
258
|
+
credentials: allCredentials,
|
|
259
|
+
envVars: allEnvVars,
|
|
260
|
+
services: Array.from(allServices),
|
|
261
|
+
unmapped: allUnmapped,
|
|
262
|
+
credentialCount: Object.values(allCredentials).reduce(
|
|
263
|
+
(sum, svc) => sum + Object.keys(svc).length, 0
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Optional live validation
|
|
268
|
+
if (sources.validate) {
|
|
269
|
+
output.validation = await validateCollected(allCredentials);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return output;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Merge helper ────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
function mergeResults(allCreds, allEnv, allServices, allUnmapped, result) {
|
|
278
|
+
for (const [svc, creds] of Object.entries(result.credentials || {})) {
|
|
279
|
+
allCreds[svc] = { ...allCreds[svc], ...creds };
|
|
280
|
+
allServices.add(svc);
|
|
281
|
+
}
|
|
282
|
+
for (const [k, v] of Object.entries(result.envVars || {})) {
|
|
283
|
+
allEnv[k] = v;
|
|
284
|
+
}
|
|
285
|
+
if (result.unmapped) allUnmapped.push(...result.unmapped);
|
|
286
|
+
}
|