rbowen commented on code in PR #2:
URL: https://github.com/apache/comdev/pull/2#discussion_r3139709886
##########
mcp/ponymail-mcp/auth.js:
##########
@@ -0,0 +1,262 @@
+/**
+ * auth.js — PonyMail session management
+ *
+ * PonyMail Foal handles OAuth entirely server-side — the auth code from ASF
OAuth
+ * can only be exchanged by PonyMail's own backend (its redirect_uri is
registered
+ * with ASF OAuth, not ours). So we can't replicate the OAuth exchange from a
CLI.
+ *
+ * Instead, this module:
+ * 1. Opens the PonyMail login page in the user's browser
+ * 2. Runs a tiny local server that waits for the user to paste their cookie
+ * OR watches for the cookie file to appear (if using browser extension)
+ * 3. Caches the session cookie to ~/.ponymail-mcp/session.json
+ *
+ * The simplest reliable flow:
+ * - Open lists.apache.org/oauth.html in the browser
+ * - User logs in (ASF LDAP)
+ * - After login, PonyMail sets a session cookie in the browser
+ * - User copies the cookie value from DevTools (or we provide a bookmarklet)
+ * - We cache it and use it for API requests
+ *
+ * Alternatively, set PONYMAIL_SESSION_COOKIE env var directly.
+ */
+
+import http from "node:http";
+import crypto from "node:crypto";
+import fs from "node:fs";
+import path from "node:path";
+import os from "node:os";
+import { exec } from "node:child_process";
+
+const SESSION_DIR = path.join(os.homedir(), ".ponymail-mcp");
+const SESSION_FILE = path.join(SESSION_DIR, "session.json");
+const CALLBACK_PORT = 39817;
+const LOGIN_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
+
+// ---------------------------------------------------------------------------
+// Session persistence
+// ---------------------------------------------------------------------------
+
+export function loadSession() {
+ try {
+ if (!fs.existsSync(SESSION_FILE)) return null;
+ const data = JSON.parse(fs.readFileSync(SESSION_FILE, "utf-8"));
+ if (data.timestamp && Date.now() - data.timestamp > 20 * 60 * 60 * 1000) {
+ console.error("[auth] Cached session expired");
+ return null;
+ }
+ return data.cookie || null;
+ } catch {
+ return null;
+ }
+}
+
+function saveSession(cookie, userInfo = {}) {
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
+ fs.writeFileSync(
+ SESSION_FILE,
+ JSON.stringify({ cookie, timestamp: Date.now(), user: userInfo }, null, 2)
+ );
+ console.error(`[auth] Session saved to ${SESSION_FILE}`);
+}
+
+export function clearSession() {
+ try {
+ if (fs.existsSync(SESSION_FILE)) {
+ fs.unlinkSync(SESSION_FILE);
+ console.error("[auth] Session cleared");
+ }
+ } catch (err) {
+ console.error("[auth] Failed to clear session:", err.message);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Browser helper
+// ---------------------------------------------------------------------------
+
+function openBrowser(url) {
+ const cmd =
+ process.platform === "darwin"
+ ? `open "${url}"`
+ : process.platform === "win32"
+ ? `start "${url}"`
+ : `xdg-open "${url}"`;
+ exec(cmd, (err) => {
+ if (err) console.error("[auth] Could not open browser:", err.message);
+ });
+}
+
+// ---------------------------------------------------------------------------
+// Login flow
+// ---------------------------------------------------------------------------
+
+/**
+ * Perform login by:
+ * 1. Opening PonyMail's login page in the browser
+ * 2. Starting a local HTTP server with a simple form where the user pastes
+ * their cookie after logging in, OR uses the bookmarklet to auto-fill
+ * 3. Saving the cookie once received
+ *
+ * @param {string} baseUrl - PonyMail base URL
+ * @param {number} [timeoutMs] - Max time to wait (default 3 min)
+ * @returns {Promise<string>} The session cookie string
+ */
+export function performLogin(baseUrl, timeoutMs = LOGIN_TIMEOUT_MS) {
+ return new Promise((resolve, reject) => {
+ let server;
+ let settled = false;
+
+ function settle(err, result) {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timer);
+ if (server) {
+ try { server.close(); } catch {}
+ }
+ if (err) reject(err);
+ else resolve(result);
+ }
+
+ const timer = setTimeout(() => {
+ settle(new Error(
+ `Login timed out after ${timeoutMs / 1000}s. Call login again to
retry.`
+ ));
+ }, timeoutMs);
+
+ server = http.createServer(async (req, res) => {
+ // Serve the cookie-paste form
+ if (req.method === "GET") {
+ res.writeHead(200, { "Content-Type": "text/html" });
+ res.end(loginPage(baseUrl));
+ return;
+ }
+
+ // Receive the pasted cookie
+ if (req.method === "POST" && req.url === "/save") {
+ let body = "";
+ for await (const chunk of req) body += chunk;
+ const params = new URLSearchParams(body);
+ const cookie = (params.get("cookie") || "").trim();
+
+ if (!cookie) {
+ res.writeHead(400, { "Content-Type": "text/html" });
+ res.end(resultPage(false, "No cookie value provided. Please try
again."));
+ return;
+ }
+
+ // Validate the cookie by calling preferences endpoint
+ try {
+ const testUrl = new URL("/api/preferences.lua", baseUrl);
+ const testResp = await fetch(testUrl.toString(), {
+ headers: { Accept: "application/json", Cookie: cookie },
+ });
+ const testData = await testResp.json();
+
+ if (testData.login && testData.login.credentials) {
+ const name = testData.login.credentials.fullname || "Unknown";
+ const email = testData.login.credentials.email || "";
+ saveSession(cookie, { fullname: name, email });
+ res.writeHead(200, { "Content-Type": "text/html" });
+ res.end(resultPage(true, `Authenticated as ${name} (${email})`));
+ settle(null, cookie);
+ } else {
+ res.writeHead(200, { "Content-Type": "text/html" });
+ res.end(resultPage(false,
+ "Cookie was accepted but no login credentials found. " +
+ "Make sure you copied the full cookie string. Try again."
+ ));
+ }
+ } catch (err) {
+ res.writeHead(500, { "Content-Type": "text/html" });
+ res.end(resultPage(false, `Validation failed: ${err.message}`));
+ }
+ return;
+ }
+
+ res.writeHead(404);
+ res.end("Not found");
+ });
+
+ server.on("error", (err) => {
+ settle(new Error(`Could not start callback server on port
${CALLBACK_PORT}: ${err.message}`));
+ });
+
+ server.listen(CALLBACK_PORT, () => {
+ // Open the local helper page (which has a link to PonyMail login +
paste form)
+ const helperUrl = `http://localhost:${CALLBACK_PORT}`;
+ console.error(`[auth] Opening login helper at ${helperUrl}`);
+ openBrowser(helperUrl);
+ });
+ });
+}
+
+// ---------------------------------------------------------------------------
+// HTML pages
+// ---------------------------------------------------------------------------
+
+function loginPage(baseUrl) {
+ const oauthUrl = `${baseUrl}/oauth.html`;
+ const hostname = new URL(baseUrl).hostname;
+ return `<!DOCTYPE html>
+<html>
+<head><title>PonyMail MCP Login</title></head>
+<body style="font-family:system-ui;max-width:640px;margin:40px auto;padding:0
20px">
+ <h1>🐴 PonyMail MCP Login</h1>
+
+ <p><strong>Step 1:</strong> Log into PonyMail (if you haven't already):</p>
+ <p><a href="${oauthUrl}" target="_blank"
style="font-size:1.2em;color:#0066cc">
+ ➜ Open ${oauthUrl}
+ </a></p>
+
+ <p><strong>Step 2:</strong> After logging in, get the session cookie. The
cookie is HttpOnly, so you need to find it via DevTools:</p>
+ <ol style="line-height:1.8">
+ <li>On <code>${hostname}</code>, open DevTools: <code>Cmd+Option+I</code>
(or <code>F12</code>)</li>
+ <li>Go to the <strong>Network</strong> tab</li>
+ <li>Reload the page (or click any link)</li>
+ <li>Click on any request to <code>${hostname}</code> (e.g., the document
or any <code>api/</code> call)</li>
+ <li>In <strong>Headers</strong> → <strong>Request Headers</strong> → find
the <strong>Cookie:</strong> line</li>
+ <li>Copy the <code>ponymail=xxxxxxxx-xxxx-...</code> part</li>
+ </ol>
+ <p>It typically looks like:<br>
+ <code style="background:#f0f0f0;padding:2px
6px">ponymail=abc123def456...</code></p>
+
+ <p><strong>Step 3:</strong> Paste it below:</p>
+ <form method="POST" action="/save">
+ <input name="cookie" type="text" placeholder="ponymail=..."
+
style="width:100%;padding:10px;font-size:1em;font-family:monospace;border:1px
solid #ccc;border-radius:4px" />
+ <br><br>
+ <button type="submit"
+ style="padding:10px
24px;font-size:1em;background:#0066cc;color:white;border:none;border-radius:4px;cursor:pointer">
+ Save Cookie
+ </button>
+ </form>
+
+ <details style="margin-top:30px">
+ <summary style="cursor:pointer;color:#0066cc">💡 Quick alternative: Console
one-liner</summary>
+ <p>On <code>${hostname}</code>, open DevTools Console and run:</p>
+ <pre
style="background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto;font-size:0.85em">fetch('/api/preferences.lua').then(r=>r.json()).then(j=>console.log(j.login?.credentials
? '✅ Logged in as: '+j.login.credentials.fullname+'\\nNow check Network tab
for any api/ request → Request Headers → Cookie' : '❌ Not logged in — log in
first'))</pre>
+ <p>This confirms you're logged in and triggers a network request so the
cookie appears in the Network tab.</p>
+ </details>
+
+ <p style="color:#888;margin-top:30px;font-size:0.9em">
+ The cookie will be saved to <code>~/.ponymail-mcp/session.json</code> and
used for API requests.
+ This page will close automatically after saving. Session expires after ~20
hours.
+ </p>
+</body>
+</html>`;
+}
+
+function resultPage(success, message) {
+ const icon = success ? "✅" : "❌";
+ const color = success ? "#2e7d32" : "#c62828";
+ return `<!DOCTYPE html>
+<html>
+<head><title>PonyMail MCP Login</title></head>
+<body style="font-family:system-ui;max-width:640px;margin:40px auto;padding:0
20px;text-align:center">
+ <h1 style="color:${color}">${icon} ${success ? "Authenticated!" : "Not
Authenticated"}</h1>
+ <p>${message}</p>
+ ${success ? "<p>You can close this tab and return to Amazon Quick.</p>" :
'<p><a href="/">← Try again</a></p>'}
Review Comment:
If you meant remove mentions of Quick, I've done that. If you meant
something else, please let me know.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]