0nmcp 1.4.0 → 1.6.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/cli.js +256 -0
- package/connections.js +4 -0
- package/engine/bundler.js +395 -0
- package/engine/cipher-portable.js +94 -0
- package/engine/index.js +390 -0
- package/engine/mapper.js +292 -0
- package/engine/parser.js +221 -0
- package/engine/platforms.js +254 -0
- package/engine/validator.js +257 -0
- package/index.js +23 -1
- package/lib/badges.json +1 -1
- package/lib/stats.json +5 -3
- package/package.json +23 -5
- package/server.js +14 -2
- package/vault/cache.js +28 -0
- package/vault/cipher.js +147 -0
- package/vault/fingerprint.js +58 -0
- package/vault/index.js +314 -0
package/engine/mapper.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Engine: Environment Variable Mapper
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Maps common environment variable names to 0nMCP service
|
|
5
|
+
// keys and credential fields. Supports exact match, pattern
|
|
6
|
+
// match, and fuzzy matching.
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
import { SERVICE_CATALOG } from "../catalog.js";
|
|
10
|
+
|
|
11
|
+
// ── Exact Env Var → Service Mapping (~75 entries) ──────────
|
|
12
|
+
|
|
13
|
+
const ENV_MAP = {
|
|
14
|
+
// Stripe
|
|
15
|
+
STRIPE_SECRET_KEY: { service: "stripe", field: "apiKey" },
|
|
16
|
+
STRIPE_API_KEY: { service: "stripe", field: "apiKey" },
|
|
17
|
+
STRIPE_KEY: { service: "stripe", field: "apiKey" },
|
|
18
|
+
STRIPE_SK: { service: "stripe", field: "apiKey" },
|
|
19
|
+
|
|
20
|
+
// OpenAI
|
|
21
|
+
OPENAI_API_KEY: { service: "openai", field: "apiKey" },
|
|
22
|
+
OPENAI_KEY: { service: "openai", field: "apiKey" },
|
|
23
|
+
|
|
24
|
+
// Slack
|
|
25
|
+
SLACK_BOT_TOKEN: { service: "slack", field: "botToken" },
|
|
26
|
+
SLACK_TOKEN: { service: "slack", field: "botToken" },
|
|
27
|
+
SLACK_OAUTH_TOKEN: { service: "slack", field: "botToken" },
|
|
28
|
+
|
|
29
|
+
// Discord
|
|
30
|
+
DISCORD_BOT_TOKEN: { service: "discord", field: "botToken" },
|
|
31
|
+
DISCORD_TOKEN: { service: "discord", field: "botToken" },
|
|
32
|
+
|
|
33
|
+
// GitHub
|
|
34
|
+
GITHUB_TOKEN: { service: "github", field: "token" },
|
|
35
|
+
GITHUB_PAT: { service: "github", field: "token" },
|
|
36
|
+
GH_TOKEN: { service: "github", field: "token" },
|
|
37
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: { service: "github", field: "token" },
|
|
38
|
+
|
|
39
|
+
// Twilio
|
|
40
|
+
TWILIO_ACCOUNT_SID: { service: "twilio", field: "accountSid" },
|
|
41
|
+
TWILIO_AUTH_TOKEN: { service: "twilio", field: "authToken" },
|
|
42
|
+
TWILIO_SID: { service: "twilio", field: "accountSid" },
|
|
43
|
+
|
|
44
|
+
// SendGrid
|
|
45
|
+
SENDGRID_API_KEY: { service: "sendgrid", field: "apiKey" },
|
|
46
|
+
SENDGRID_KEY: { service: "sendgrid", field: "apiKey" },
|
|
47
|
+
|
|
48
|
+
// Resend
|
|
49
|
+
RESEND_API_KEY: { service: "resend", field: "apiKey" },
|
|
50
|
+
RESEND_KEY: { service: "resend", field: "apiKey" },
|
|
51
|
+
|
|
52
|
+
// Airtable
|
|
53
|
+
AIRTABLE_API_KEY: { service: "airtable", field: "apiKey" },
|
|
54
|
+
AIRTABLE_PAT: { service: "airtable", field: "apiKey" },
|
|
55
|
+
AIRTABLE_TOKEN: { service: "airtable", field: "apiKey" },
|
|
56
|
+
|
|
57
|
+
// Notion
|
|
58
|
+
NOTION_API_KEY: { service: "notion", field: "apiKey" },
|
|
59
|
+
NOTION_TOKEN: { service: "notion", field: "apiKey" },
|
|
60
|
+
NOTION_INTEGRATION_TOKEN: { service: "notion", field: "apiKey" },
|
|
61
|
+
|
|
62
|
+
// Linear
|
|
63
|
+
LINEAR_API_KEY: { service: "linear", field: "apiKey" },
|
|
64
|
+
LINEAR_TOKEN: { service: "linear", field: "apiKey" },
|
|
65
|
+
|
|
66
|
+
// Shopify
|
|
67
|
+
SHOPIFY_ACCESS_TOKEN: { service: "shopify", field: "accessToken" },
|
|
68
|
+
SHOPIFY_ADMIN_TOKEN: { service: "shopify", field: "accessToken" },
|
|
69
|
+
SHOPIFY_STORE: { service: "shopify", field: "store" },
|
|
70
|
+
SHOPIFY_STORE_DOMAIN: { service: "shopify", field: "store" },
|
|
71
|
+
|
|
72
|
+
// HubSpot
|
|
73
|
+
HUBSPOT_ACCESS_TOKEN: { service: "hubspot", field: "accessToken" },
|
|
74
|
+
HUBSPOT_API_KEY: { service: "hubspot", field: "accessToken" },
|
|
75
|
+
HUBSPOT_TOKEN: { service: "hubspot", field: "accessToken" },
|
|
76
|
+
|
|
77
|
+
// Supabase
|
|
78
|
+
SUPABASE_KEY: { service: "supabase", field: "apiKey" },
|
|
79
|
+
SUPABASE_ANON_KEY: { service: "supabase", field: "apiKey" },
|
|
80
|
+
SUPABASE_SERVICE_KEY: { service: "supabase", field: "apiKey" },
|
|
81
|
+
SUPABASE_SERVICE_ROLE_KEY: { service: "supabase", field: "apiKey" },
|
|
82
|
+
SUPABASE_API_KEY: { service: "supabase", field: "apiKey" },
|
|
83
|
+
SUPABASE_PROJECT_REF: { service: "supabase", field: "projectRef" },
|
|
84
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: { service: "supabase", field: "apiKey" },
|
|
85
|
+
NEXT_PUBLIC_SUPABASE_URL: { service: "supabase", field: "projectRef", transform: "extractSupabaseRef" },
|
|
86
|
+
|
|
87
|
+
// Calendly
|
|
88
|
+
CALENDLY_API_KEY: { service: "calendly", field: "apiKey" },
|
|
89
|
+
CALENDLY_TOKEN: { service: "calendly", field: "apiKey" },
|
|
90
|
+
|
|
91
|
+
// Google Services
|
|
92
|
+
GOOGLE_ACCESS_TOKEN: { service: "google_calendar", field: "access_token" },
|
|
93
|
+
GMAIL_ACCESS_TOKEN: { service: "gmail", field: "access_token" },
|
|
94
|
+
GOOGLE_SHEETS_TOKEN: { service: "google_sheets", field: "access_token" },
|
|
95
|
+
GOOGLE_SHEETS_ACCESS_TOKEN: { service: "google_sheets", field: "access_token" },
|
|
96
|
+
GOOGLE_DRIVE_TOKEN: { service: "google_drive", field: "access_token" },
|
|
97
|
+
GOOGLE_DRIVE_ACCESS_TOKEN: { service: "google_drive", field: "access_token" },
|
|
98
|
+
GOOGLE_CALENDAR_ACCESS_TOKEN: { service: "google_calendar", field: "access_token" },
|
|
99
|
+
|
|
100
|
+
// Jira
|
|
101
|
+
JIRA_EMAIL: { service: "jira", field: "email" },
|
|
102
|
+
JIRA_API_TOKEN: { service: "jira", field: "apiToken" },
|
|
103
|
+
JIRA_TOKEN: { service: "jira", field: "apiToken" },
|
|
104
|
+
JIRA_DOMAIN: { service: "jira", field: "domain" },
|
|
105
|
+
ATLASSIAN_EMAIL: { service: "jira", field: "email" },
|
|
106
|
+
ATLASSIAN_API_TOKEN: { service: "jira", field: "apiToken" },
|
|
107
|
+
|
|
108
|
+
// Zendesk
|
|
109
|
+
ZENDESK_EMAIL: { service: "zendesk", field: "email" },
|
|
110
|
+
ZENDESK_API_TOKEN: { service: "zendesk", field: "apiToken" },
|
|
111
|
+
ZENDESK_TOKEN: { service: "zendesk", field: "apiToken" },
|
|
112
|
+
ZENDESK_SUBDOMAIN: { service: "zendesk", field: "subdomain" },
|
|
113
|
+
|
|
114
|
+
// Mailchimp
|
|
115
|
+
MAILCHIMP_API_KEY: { service: "mailchimp", field: "apiKey" },
|
|
116
|
+
MAILCHIMP_KEY: { service: "mailchimp", field: "apiKey" },
|
|
117
|
+
|
|
118
|
+
// Zoom
|
|
119
|
+
ZOOM_ACCESS_TOKEN: { service: "zoom", field: "access_token" },
|
|
120
|
+
ZOOM_TOKEN: { service: "zoom", field: "access_token" },
|
|
121
|
+
|
|
122
|
+
// Microsoft 365
|
|
123
|
+
MICROSOFT_ACCESS_TOKEN: { service: "microsoft", field: "access_token" },
|
|
124
|
+
MS_ACCESS_TOKEN: { service: "microsoft", field: "access_token" },
|
|
125
|
+
AZURE_AD_TOKEN: { service: "microsoft", field: "access_token" },
|
|
126
|
+
|
|
127
|
+
// MongoDB
|
|
128
|
+
MONGODB_API_KEY: { service: "mongodb", field: "apiKey" },
|
|
129
|
+
MONGODB_APP_ID: { service: "mongodb", field: "appId" },
|
|
130
|
+
MONGO_API_KEY: { service: "mongodb", field: "apiKey" },
|
|
131
|
+
MONGO_APP_ID: { service: "mongodb", field: "appId" },
|
|
132
|
+
|
|
133
|
+
// CRM
|
|
134
|
+
CRM_ACCESS_TOKEN: { service: "crm", field: "access_token" },
|
|
135
|
+
CRM_TOKEN: { service: "crm", field: "access_token" },
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ── Service name aliases for fuzzy matching ────────────────
|
|
139
|
+
|
|
140
|
+
const SERVICE_ALIASES = {
|
|
141
|
+
stripe_api: "stripe",
|
|
142
|
+
open_ai: "openai",
|
|
143
|
+
send_grid: "sendgrid",
|
|
144
|
+
air_table: "airtable",
|
|
145
|
+
hub_spot: "hubspot",
|
|
146
|
+
mail_chimp: "mailchimp",
|
|
147
|
+
google_cal: "google_calendar",
|
|
148
|
+
gcal: "google_calendar",
|
|
149
|
+
gsheets: "google_sheets",
|
|
150
|
+
gdrive: "google_drive",
|
|
151
|
+
ms365: "microsoft",
|
|
152
|
+
outlook: "microsoft",
|
|
153
|
+
teams: "microsoft",
|
|
154
|
+
onedrive: "microsoft",
|
|
155
|
+
mongo: "mongodb",
|
|
156
|
+
gh: "github",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ── Value transformations ──────────────────────────────────
|
|
160
|
+
|
|
161
|
+
const TRANSFORMS = {
|
|
162
|
+
extractSupabaseRef(value) {
|
|
163
|
+
// Extract project ref from Supabase URL: https://{ref}.supabase.co
|
|
164
|
+
const match = value.match(/https?:\/\/([a-z0-9]+)\.supabase\.co/);
|
|
165
|
+
return match ? match[1] : value;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Map parsed entries to 0nMCP services.
|
|
171
|
+
* @param {Array<{ key: string, value: string }>} entries
|
|
172
|
+
* @returns {{ mapped: Array<MappedCredential>, unmapped: Array<{ key: string, value: string }> }}
|
|
173
|
+
*/
|
|
174
|
+
export function mapEnvVars(entries) {
|
|
175
|
+
const mapped = [];
|
|
176
|
+
const unmapped = [];
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
// Check for direct JSON path format: "stripe.apiKey"
|
|
180
|
+
if (entry.key.includes(".")) {
|
|
181
|
+
const [service, field] = entry.key.split(".", 2);
|
|
182
|
+
if (SERVICE_CATALOG[service]) {
|
|
183
|
+
mapped.push({
|
|
184
|
+
service,
|
|
185
|
+
field,
|
|
186
|
+
value: entry.value,
|
|
187
|
+
envVar: entry.key,
|
|
188
|
+
confidence: "exact",
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Exact match
|
|
195
|
+
const upperKey = entry.key.toUpperCase();
|
|
196
|
+
const exact = ENV_MAP[upperKey] || ENV_MAP[entry.key];
|
|
197
|
+
if (exact) {
|
|
198
|
+
let value = entry.value;
|
|
199
|
+
if (exact.transform && TRANSFORMS[exact.transform]) {
|
|
200
|
+
value = TRANSFORMS[exact.transform](value);
|
|
201
|
+
}
|
|
202
|
+
mapped.push({
|
|
203
|
+
service: exact.service,
|
|
204
|
+
field: exact.field,
|
|
205
|
+
value,
|
|
206
|
+
envVar: entry.key,
|
|
207
|
+
confidence: "exact",
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pattern match: {SERVICE}_API_KEY, {SERVICE}_SECRET_KEY, {SERVICE}_TOKEN
|
|
213
|
+
const patternResult = patternMatch(upperKey, entry.value);
|
|
214
|
+
if (patternResult) {
|
|
215
|
+
mapped.push({ ...patternResult, envVar: entry.key, confidence: "pattern" });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
unmapped.push(entry);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { mapped, unmapped };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Try pattern matching against known service names.
|
|
227
|
+
*/
|
|
228
|
+
function patternMatch(key, value) {
|
|
229
|
+
const patterns = [
|
|
230
|
+
{ regex: /^([A-Z_]+?)_(?:SECRET_KEY|API_KEY|APIKEY)$/i, field: "apiKey" },
|
|
231
|
+
{ regex: /^([A-Z_]+?)_(?:BOT_TOKEN|BOT_KEY)$/i, field: "botToken" },
|
|
232
|
+
{ regex: /^([A-Z_]+?)_(?:ACCESS_TOKEN|OAUTH_TOKEN)$/i, field: "access_token" },
|
|
233
|
+
{ regex: /^([A-Z_]+?)_(?:AUTH_TOKEN)$/i, field: "authToken" },
|
|
234
|
+
{ regex: /^([A-Z_]+?)_(?:TOKEN|KEY)$/i, field: "apiKey" },
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
for (const { regex, field } of patterns) {
|
|
238
|
+
const match = key.match(regex);
|
|
239
|
+
if (!match) continue;
|
|
240
|
+
|
|
241
|
+
const rawName = match[1].toLowerCase().replace(/_/g, "");
|
|
242
|
+
// Check catalog directly
|
|
243
|
+
if (SERVICE_CATALOG[rawName]) {
|
|
244
|
+
return { service: rawName, field, value };
|
|
245
|
+
}
|
|
246
|
+
// Check aliases
|
|
247
|
+
const alias = SERVICE_ALIASES[match[1].toLowerCase()] || SERVICE_ALIASES[rawName];
|
|
248
|
+
if (alias && SERVICE_CATALOG[alias]) {
|
|
249
|
+
return { service: alias, field, value };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Group mapped credentials by service.
|
|
258
|
+
* @param {Array<MappedCredential>} mapped
|
|
259
|
+
* @returns {Record<string, { credentials: Record<string, string>, envVars: string[] }>}
|
|
260
|
+
*/
|
|
261
|
+
export function groupByService(mapped) {
|
|
262
|
+
const groups = {};
|
|
263
|
+
|
|
264
|
+
for (const m of mapped) {
|
|
265
|
+
if (!groups[m.service]) {
|
|
266
|
+
groups[m.service] = { credentials: {}, envVars: [] };
|
|
267
|
+
}
|
|
268
|
+
groups[m.service].credentials[m.field] = m.value;
|
|
269
|
+
groups[m.service].envVars.push(m.envVar);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return groups;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Validate that all required credential keys are present for a service.
|
|
277
|
+
* @param {string} service
|
|
278
|
+
* @param {Record<string, string>} credentials
|
|
279
|
+
* @returns {{ valid: boolean, missing: string[], extra: string[] }}
|
|
280
|
+
*/
|
|
281
|
+
export function validateMapping(service, credentials) {
|
|
282
|
+
const catalog = SERVICE_CATALOG[service];
|
|
283
|
+
if (!catalog) return { valid: false, missing: [], extra: [], error: `Unknown service: ${service}` };
|
|
284
|
+
|
|
285
|
+
const required = catalog.credentialKeys || [];
|
|
286
|
+
const provided = Object.keys(credentials);
|
|
287
|
+
|
|
288
|
+
const missing = required.filter(k => !credentials[k]);
|
|
289
|
+
const extra = provided.filter(k => !required.includes(k));
|
|
290
|
+
|
|
291
|
+
return { valid: missing.length === 0, missing, extra };
|
|
292
|
+
}
|
package/engine/parser.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Engine: Credential File Parser
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Parses .env, CSV, and JSON credential files into a
|
|
5
|
+
// normalized array of { key, value } entries.
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { extname } from "path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse a .env file into key-value entries.
|
|
13
|
+
* Handles: comments (#), blank lines, quoted values, `export` prefix.
|
|
14
|
+
* @param {string} filePath
|
|
15
|
+
* @returns {Array<{ key: string, value: string }>}
|
|
16
|
+
*/
|
|
17
|
+
export function parseEnvFile(filePath) {
|
|
18
|
+
const content = readFileSync(filePath, "utf-8");
|
|
19
|
+
return parseEnvString(content);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse .env content from a string.
|
|
24
|
+
* @param {string} content
|
|
25
|
+
* @returns {Array<{ key: string, value: string }>}
|
|
26
|
+
*/
|
|
27
|
+
export function parseEnvString(content) {
|
|
28
|
+
const entries = [];
|
|
29
|
+
const lines = content.split(/\r?\n/);
|
|
30
|
+
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
34
|
+
|
|
35
|
+
// Strip `export ` prefix
|
|
36
|
+
const clean = trimmed.startsWith("export ") ? trimmed.slice(7) : trimmed;
|
|
37
|
+
|
|
38
|
+
const eqIdx = clean.indexOf("=");
|
|
39
|
+
if (eqIdx === -1) continue;
|
|
40
|
+
|
|
41
|
+
const key = clean.slice(0, eqIdx).trim();
|
|
42
|
+
let value = clean.slice(eqIdx + 1).trim();
|
|
43
|
+
|
|
44
|
+
// Strip surrounding quotes
|
|
45
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
46
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
47
|
+
value = value.slice(1, -1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Strip inline comments (only for unquoted values)
|
|
51
|
+
if (!clean.slice(eqIdx + 1).trim().startsWith('"') &&
|
|
52
|
+
!clean.slice(eqIdx + 1).trim().startsWith("'")) {
|
|
53
|
+
const commentIdx = value.indexOf(" #");
|
|
54
|
+
if (commentIdx > -1) value = value.slice(0, commentIdx).trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (key) entries.push({ key, value });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse a CSV file into key-value entries.
|
|
65
|
+
* Expects columns: key,value (with optional header row).
|
|
66
|
+
* @param {string} filePath
|
|
67
|
+
* @returns {Array<{ key: string, value: string }>}
|
|
68
|
+
*/
|
|
69
|
+
export function parseCsvFile(filePath) {
|
|
70
|
+
const content = readFileSync(filePath, "utf-8");
|
|
71
|
+
return parseCsvString(content);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse CSV content from a string.
|
|
76
|
+
* @param {string} content
|
|
77
|
+
* @returns {Array<{ key: string, value: string }>}
|
|
78
|
+
*/
|
|
79
|
+
export function parseCsvString(content) {
|
|
80
|
+
const entries = [];
|
|
81
|
+
const lines = content.split(/\r?\n/).filter(l => l.trim());
|
|
82
|
+
if (lines.length === 0) return entries;
|
|
83
|
+
|
|
84
|
+
// Auto-detect and skip header row
|
|
85
|
+
const first = lines[0].toLowerCase();
|
|
86
|
+
const startIdx = (first.includes("key") && first.includes("value")) ? 1 : 0;
|
|
87
|
+
|
|
88
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
89
|
+
const line = lines[i].trim();
|
|
90
|
+
if (!line) continue;
|
|
91
|
+
|
|
92
|
+
// Simple CSV parse (handles quoted fields)
|
|
93
|
+
const fields = parseCsvLine(line);
|
|
94
|
+
if (fields.length >= 2) {
|
|
95
|
+
entries.push({ key: fields[0].trim(), value: fields[1].trim() });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse a single CSV line, handling quoted fields.
|
|
104
|
+
*/
|
|
105
|
+
function parseCsvLine(line) {
|
|
106
|
+
const fields = [];
|
|
107
|
+
let current = "";
|
|
108
|
+
let inQuotes = false;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < line.length; i++) {
|
|
111
|
+
const ch = line[i];
|
|
112
|
+
if (ch === '"') {
|
|
113
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
114
|
+
current += '"';
|
|
115
|
+
i++;
|
|
116
|
+
} else {
|
|
117
|
+
inQuotes = !inQuotes;
|
|
118
|
+
}
|
|
119
|
+
} else if (ch === "," && !inQuotes) {
|
|
120
|
+
fields.push(current);
|
|
121
|
+
current = "";
|
|
122
|
+
} else {
|
|
123
|
+
current += ch;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
fields.push(current);
|
|
127
|
+
return fields;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a JSON file into key-value entries.
|
|
132
|
+
* Supports flat objects, nested service objects, and array format.
|
|
133
|
+
* @param {string} filePath
|
|
134
|
+
* @returns {Array<{ key: string, value: string }>}
|
|
135
|
+
*/
|
|
136
|
+
export function parseJsonFile(filePath) {
|
|
137
|
+
const content = readFileSync(filePath, "utf-8");
|
|
138
|
+
return parseJsonString(content);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse JSON content from a string.
|
|
143
|
+
* @param {string} content
|
|
144
|
+
* @returns {Array<{ key: string, value: string }>}
|
|
145
|
+
*/
|
|
146
|
+
export function parseJsonString(content) {
|
|
147
|
+
const data = JSON.parse(content);
|
|
148
|
+
const entries = [];
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(data)) {
|
|
151
|
+
// Array format: [{ service: "stripe", credentials: { apiKey: "..." } }]
|
|
152
|
+
for (const item of data) {
|
|
153
|
+
if (item.service && item.credentials) {
|
|
154
|
+
for (const [field, val] of Object.entries(item.credentials)) {
|
|
155
|
+
entries.push({ key: `${item.service}.${field}`, value: String(val) });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (typeof data === "object") {
|
|
160
|
+
for (const [key, val] of Object.entries(data)) {
|
|
161
|
+
if (typeof val === "object" && val !== null) {
|
|
162
|
+
// Nested: { stripe: { apiKey: "..." } }
|
|
163
|
+
for (const [field, v] of Object.entries(val)) {
|
|
164
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
165
|
+
entries.push({ key: `${key}.${field}`, value: String(v) });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} else if (typeof val === "string" || typeof val === "number") {
|
|
169
|
+
// Flat: { STRIPE_SECRET_KEY: "sk_..." }
|
|
170
|
+
entries.push({ key, value: String(val) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Auto-detect file format from extension or content.
|
|
180
|
+
* @param {string} filePath
|
|
181
|
+
* @returns {"env" | "csv" | "json" | "unknown"}
|
|
182
|
+
*/
|
|
183
|
+
export function detectFormat(filePath) {
|
|
184
|
+
const ext = extname(filePath).toLowerCase();
|
|
185
|
+
if (ext === ".env") return "env";
|
|
186
|
+
if (ext === ".csv") return "csv";
|
|
187
|
+
if (ext === ".json") return "json";
|
|
188
|
+
|
|
189
|
+
// Check filename patterns
|
|
190
|
+
const name = filePath.split("/").pop() || "";
|
|
191
|
+
if (name.startsWith(".env")) return "env";
|
|
192
|
+
|
|
193
|
+
// Inspect content
|
|
194
|
+
try {
|
|
195
|
+
const content = readFileSync(filePath, "utf-8").trim();
|
|
196
|
+
if (content.startsWith("{") || content.startsWith("[")) return "json";
|
|
197
|
+
if (content.includes(",") && content.split("\n")[0].split(",").length >= 2) return "csv";
|
|
198
|
+
if (/^[A-Z_]+=/.test(content)) return "env";
|
|
199
|
+
} catch {
|
|
200
|
+
// Fall through
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return "unknown";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Auto-detect format and parse any supported file.
|
|
208
|
+
* @param {string} filePath
|
|
209
|
+
* @returns {{ format: string, entries: Array<{ key: string, value: string }> }}
|
|
210
|
+
*/
|
|
211
|
+
export function parseFile(filePath) {
|
|
212
|
+
const format = detectFormat(filePath);
|
|
213
|
+
|
|
214
|
+
switch (format) {
|
|
215
|
+
case "env": return { format, entries: parseEnvFile(filePath) };
|
|
216
|
+
case "csv": return { format, entries: parseCsvFile(filePath) };
|
|
217
|
+
case "json": return { format, entries: parseJsonFile(filePath) };
|
|
218
|
+
default:
|
|
219
|
+
throw new Error(`Unknown file format for: ${filePath}. Supported: .env, .csv, .json`);
|
|
220
|
+
}
|
|
221
|
+
}
|