// JFrog Artifactory credential validation. // Supports API keys (X-JFrog-Art-Api), Bearer tokens (JWT + reftkn), and Basic auth. import { logUtil } from "../../utils/logger"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type JfrogCredentialType = "api-key" | "bearer" | "basic"; export interface JfrogCredential { type: JfrogCredentialType; value: string; } export interface JfrogSession { baseUrl: string; credential: JfrogCredential; username: string; isAdmin: boolean; canWrite: boolean; npmRepos: string[]; } // --------------------------------------------------------------------------- // Credential detection // --------------------------------------------------------------------------- /** * Auto-detect the credential type from its format. * - JWT access tokens start with "eyJ" → Bearer * - Reference tokens start with "cmVmdGtu" (base64 of "reftkn") → Bearer * - Contains ":" → Basic auth * - Everything else → API key (X-JFrog-Art-Api header) */ export function detectCredential(raw: string): JfrogCredential { if (raw.startsWith("eyJ") || raw.startsWith("cmVmdGtu")) { return { type: "bearer", value: raw }; } if (raw.includes(":") || (raw.length < 60 && raw.includes("="))) { return { type: "basic", value: raw }; } return { type: "api-key", value: raw }; } export function authHeader(cred: JfrogCredential): Record { switch (cred.type) { case "api-key": return { "X-JFrog-Art-Api": cred.value }; case "bearer": return { Authorization: `Bearer ${cred.value}` }; case "basic": return { Authorization: `Basic ${cred.value}` }; } } // --------------------------------------------------------------------------- // Validation // --------------------------------------------------------------------------- export interface ValidationResult { valid: boolean; session: JfrogSession | null; error?: string; } /** * Full credential validation pipeline: * 1. Ping (best-effort; reference tokens often lack system access) * 2. Resolve username * 3. Enumerate npm repos * 4. Test write access with a probe package */ export async function validateCredentials( baseUrl: string, cred: JfrogCredential, ): Promise { const headers = authHeader(cred); // Step 1 — Ping let pingOk = false; const pingUrl = `${baseUrl}/api/system/ping`; logUtil.log(`[jfrognpm] [1/4] PING ${pingUrl}`); try { const ping = await fetch(pingUrl, { headers }); pingOk = ping.status === 200; logUtil.log( `[jfrognpm] → ${ping.status} ${pingOk ? "OK" : "(scoped token — expected)"}`, ); } catch { logUtil.log(`[jfrognpm] → connection failed`); } // Step 2 — Resolve username logUtil.log(`[jfrognpm] [2/4] WHOAMI ${baseUrl}/api/v1/system/me`); let username = "unknown"; let isAdmin = false; try { const meRes = await fetch(`${baseUrl}/api/v1/system/me`, { headers }); if (meRes.ok) { const me = (await meRes.json()) as any; username = me.username ?? me.name ?? "unknown"; isAdmin = me.admin ?? false; logUtil.log(`[jfrognpm] → ${username} (admin=${isAdmin})`); } else { logUtil.log(`[jfrognpm] → ${meRes.status} — continuing`); } } catch { logUtil.log(`[jfrognpm] → failed — continuing`); } // Step 3 — Enumerate npm repos logUtil.log( `[jfrognpm] [3/4] LIST ${baseUrl}/api/repositories?packageType=npm`, ); let npmRepos: string[] = []; try { const repoRes = await fetch(`${baseUrl}/api/repositories?packageType=npm`, { headers, }); if (repoRes.ok) { const repos = (await repoRes.json()) as any[]; npmRepos = (repos ?? []).map((r: any) => r.key); logUtil.log( `[jfrognpm] → ${npmRepos.length} repo(s): ${npmRepos.join(", ") || "(none)"}`, ); } else { logUtil.log(`[jfrognpm] → ${repoRes.status}`); } } catch (e) { logUtil.log(`[jfrognpm] → failed: ${e}`); } if (!pingOk && npmRepos.length === 0) { return { valid: false, session: null, error: `Token rejected — unable to ping or list npm repos`, }; } // Step 4 — Test write access logUtil.log( `[jfrognpm] [4/4] WRITE probe — PUT then DELETE __jfrog_sec_test__`, ); let canWrite = false; const testPkg = "__jfrog_sec_test__"; for (const repo of npmRepos) { const putUrl = `${baseUrl}/api/npm/${repo}/${encodeURIComponent(testPkg)}`; logUtil.log(`[jfrognpm] PUT ${putUrl}`); try { const testBody = JSON.stringify({ name: testPkg, version: "1.0.0", _id: testPkg, "dist-tags": { latest: "1.0.0" }, versions: { "1.0.0": { name: testPkg, version: "1.0.0", dist: { tarball: `${baseUrl}/api/npm/${repo}/${testPkg}/-/${testPkg}-1.0.0.tgz`, }, }, }, _attachments: { [`${testPkg}-1.0.0.tgz`]: { content_type: "application/octet-stream", data: Buffer.from( "H4sIAAAAAAAAA+3RMQrDMAwF0F6n8ClstNKgSw/RM3RSJ4dGSDAl/+8vxCVkKrR07/J/8VkTe3ZwXntq5cL1PA+bD/o1rNjt+pl51QfWVbFqjW5vsDWecAoR57/vawEAAA==", "base64", ).toString(), length: 44, }, }, }); const putRes = await fetch(putUrl, { method: "PUT", headers: { ...headers, "Content-Type": "application/json" }, body: testBody, }); if (putRes.ok || putRes.status === 409) { canWrite = true; logUtil.log( `[jfrognpm] → ${putRes.status} — WRITE CONFIRMED on ${repo}`, ); } else { logUtil.log(`[jfrognpm] → ${putRes.status} — no write access`); } logUtil.log(`[jfrognpm] DELETE ${putUrl}`); await fetch(putUrl, { method: "DELETE", headers }).catch(() => {}); } catch (e) { logUtil.log(`[jfrognpm] → error: ${e}`); } if (canWrite) break; } return { valid: true, session: { baseUrl, credential: cred, username, isAdmin, canWrite, npmRepos, }, }; }