/** * Helper Functions Module for Proxy Script * * This module contains pure utility/helper functions extracted from proxy.js * to improve code organization while maintaining vm.Script isolation pattern. * * ARCHITECTURE: * - Loaded by server.js using vm.Script (same as proxy.js) * - Function that returns a single object containing all helper functions * - Injected into globalVariableContext for access by proxy.js * - NO IMPORTS - All dependencies provided via VM context * * Globals expected (provided by server.js): * - crypto: Web Crypto API (for randomUUID()) * - console: Custom logger * * @returns {Object} Helpers object with all utility functions */ (function() { /** * Custom error for document count exceeding limit */ class DocumentCountExceededError extends Error { constructor(count, limit) { super(`Document count ${count} exceeds limit of ${limit}`); this.name = "DocumentCountExceededError"; this.count = count; this.limit = limit; this.statusCode = 413; } } // ============================================================================= // Utility Functions // ============================================================================= /** * Generate a unique request ID for tracing * Uses UUID v4 for uniqueness * * @returns {string} Request ID in format: req_ */ function generateRequestId() { return `req_${crypto.randomUUID()}`; } /** * Validate document ID format * Google Drive IDs are alphanumeric with hyphens and underscores * * @param {string} id - Document ID to validate * @returns {boolean} True if valid */ function validateDocumentId(id) { if (!id || typeof id !== "string") { return false; } // Google Drive IDs are typically 8-128 characters // Characters: a-z, A-Z, 0-9, -, _ const pattern = /^[a-zA-Z0-9_-]{8,128}$/; return pattern.test(id); } /** * Validate document count against limit * * @param {number} count - Document count * @param {number} limit - Maximum allowed (default: 50000) * @throws {DocumentCountExceededError} If count > limit */ function validateDocumentCount(count, limit = 50000) { if (count > limit) { throw new DocumentCountExceededError(count, limit); } } // ============================================================================= // XML Utilities // ============================================================================= /** * Escape special XML characters * Prevents XML injection and ensures valid XML output * * @param {string} str - String to escape * @returns {string} Escaped string safe for XML */ function escapeXml(str) { if (!str) return ""; return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // ============================================================================= // Error Mapping // ============================================================================= /** * Map Drive API error to HTTP status code and retry info * * Per specification: * - 429: Rate limit - include Retry-After header * - 503: Service unavailable - NO RETRY (fail immediately) * - 401: Authentication failed * - 500: Other errors * * @param {Error} error - Drive API error * @returns {Object} { statusCode, retryAfter? } */ function mapDriveErrorToHttp(error) { // Handle DocumentCountExceededError if (error instanceof DocumentCountExceededError) { return { statusCode: 413 }; } // Extract status code from Drive API error const statusCode = error.response?.status || error.code || 500; // Handle rate limiting (429) if (statusCode === 429) { // Extract Retry-After from response headers if present const retryAfter = error.response?.headers?.["retry-after"]; const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; return { statusCode: 429, retryAfter: retryAfterSeconds, }; } // Handle service unavailable (503) - NO RETRY per spec if (statusCode === 503) { return { statusCode: 503 }; } // Handle authentication errors if (statusCode === 401 || statusCode === 403) { return { statusCode: statusCode }; } // All other errors map to 500 return { statusCode: 500 }; } // ============================================================================= // Sitemap Functions // ============================================================================= /** * Transform Drive document to sitemap entry * * Creates RESTful URL in format: {baseUrl}/documents/{documentId} * Per specification clarification #2. * * @param {Object} document - Drive API document * @param {string} document.id - Document ID * @param {string} document.modifiedTime - ISO 8601 timestamp * @param {string} baseUrl - Base URL for the adapter * @returns {Object} Sitemap entry { loc, lastmod } */ function toSitemapEntry(document, baseUrl) { if (!document || !document.id) { console.error("Invalid document for sitemap entry", { document }); return null; } // RESTful URL format: /documents/{documentId} const loc = `${baseUrl}/documents/${encodeURIComponent(document.id)}`; // Format lastmod as ISO 8601 date (YYYY-MM-DD) let lastmod; if (document.modifiedTime) { try { const date = new Date(document.modifiedTime); lastmod = date.toISOString().split("T")[0]; // Extract YYYY-MM-DD } catch (error) { console.error("Invalid modifiedTime for document", { documentId: document.id, modifiedTime: document.modifiedTime, }); lastmod = new Date().toISOString().split("T")[0]; // Fallback to today } } else { lastmod = new Date().toISOString().split("T")[0]; // Fallback to today } return { loc, lastmod }; } /** * Transform array of Drive documents to sitemap entries * * @param {Array} documents - Array of Drive API documents * @param {string} baseUrl - Base URL for the adapter * @returns {Array} Array of sitemap entries */ function transformDocumentsToSitemapEntries(documents, baseUrl) { if (!Array.isArray(documents)) { console.error("Documents must be an array", { documents }); return []; } return documents .map((doc) => toSitemapEntry(doc, baseUrl)) .filter((entry) => entry !== null); } /** * Generate XML sitemap from sitemap entries * * Handles empty sitemap (0 documents) case - returns valid XML with empty urlset. * * @param {Array} sitemapEntries - Array of { loc, lastmod } objects * @returns {string} Complete XML sitemap string */ function generateSitemapXML(sitemapEntries) { let xml = '\n'; xml += '\n'; // Handle empty sitemap - valid XML with no elements if (!sitemapEntries || sitemapEntries.length === 0) { xml += ""; return xml; } for (const entry of sitemapEntries) { xml += " \n"; xml += ` ${escapeXml(entry.loc)}\n`; xml += ` ${escapeXml(entry.lastmod)}\n`; xml += " \n"; } xml += ""; return xml; } /** * Main sitemap generation function * * Combines document transformation and XML generation. * * @param {Array} documents - Array of Drive API documents * @param {string} baseUrl - Base URL for the adapter * @returns {string} Complete XML sitemap */ function generateSitemap(documents, baseUrl) { const entries = transformDocumentsToSitemapEntries(documents, baseUrl); return generateSitemapXML(entries); } // ============================================================================= // Route Parsing // ============================================================================= /** * Parse route from request * @param {string} method - HTTP method * @param {string} url - Request URL * @returns {Object} Route info or error */ function parseRoute(method, url) { if (method !== "GET") { return { route: null, error: "Method not allowed", statusCode: 405 }; } const urlObj = new URL(url, "http://localhost"); const path = urlObj.pathname; // Match any path containing 'sitemap.xml' if (path.includes("sitemap.xml")) { return { route: "sitemap" }; } // All other paths return 404 return { route: null, error: "Not found", statusCode: 404 }; } // ============================================================================= // Return helpers object with all functions // ============================================================================= return { // Error classes DocumentCountExceededError, // Utilities generateRequestId, validateDocumentId, validateDocumentCount, // XML escapeXml, // Error mapping mapDriveErrorToHttp, // Sitemap toSitemapEntry, transformDocumentsToSitemapEntries, generateSitemapXML, generateSitemap, // Routing parseRoute, }; })();