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.
@@ -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
+ }
@@ -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
+ }