From 628d3a153bc633322f3fda08202a39cdeb662005 Mon Sep 17 00:00:00 2001 From: Bill Stumbo Date: Sun, 3 May 2026 11:52:11 -0400 Subject: [PATCH 1/4] Initial implementation of AI assisted search for Interlisp.org This provides the basic Hugo infrastructure updates to retrieve AI assisted search results and display them. In addition, the new directory search-function contains the code used to implement a cloud function that acts as an intermediary between Hugo and the Vertex AI search function. It acts as a gatekeeper allowing request only from specific websites and coordinates the structuring of the results with Hugo to allow display. This demonstrates end to end functionality. The results are somewhat rudimentary, the AI is only using the contents of the web page to address the query. --- assets/js/vertex-search.js | 122 ++ assets/scss/_styles_project.scss | 83 + config/_default/params.yaml | 9 + layouts/_partials/hooks/head-end.html | 3 + layouts/search.html | 33 + search-function/.gcloudignore | 3 + search-function/index.js | 163 ++ search-function/package-lock.json | 1368 +++++++++++++++++ search-function/package.json | 18 + search-function/rateLimiter.js | 101 ++ .../vertex-ai-search-implementation.md | 715 +++++++++ 11 files changed, 2618 insertions(+) create mode 100644 assets/js/vertex-search.js create mode 100644 layouts/search.html create mode 100644 search-function/.gcloudignore create mode 100644 search-function/index.js create mode 100644 search-function/package-lock.json create mode 100644 search-function/package.json create mode 100644 search-function/rateLimiter.js create mode 100644 search-function/vertex-ai-search-implementation.md diff --git a/assets/js/vertex-search.js b/assets/js/vertex-search.js new file mode 100644 index 00000000..3bf4944c --- /dev/null +++ b/assets/js/vertex-search.js @@ -0,0 +1,122 @@ +(function () { + 'use strict'; + + // Only run on the search results page + const config = document.getElementById('vertex-search-config'); + if (!config) return; + + const FUNCTION_URL = config.dataset.searchUrl; + if (!FUNCTION_URL) { + console.warn('Vertex search URL not configured'); + return; + } + + // Get query from URL params (?q=...) + const urlParams = new URLSearchParams(window.location.search); + const query = urlParams.get('q') || ''; + + const statusEl = document.getElementById('vertex-search-status'); + const hitsEl = document.getElementById('vertex-search-hits'); + const summaryEl = document.getElementById('vertex-search-summary'); + const summaryTxt = document.getElementById('summary-text'); + + function escapeHtml(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + async function doSearch(q) { + if (!q) { + statusEl.textContent = 'Enter a search query above.'; + return; + } + + // Update page title to reflect query + document.title = `Search: ${q}`; + statusEl.innerHTML = ' Searching for ' + + escapeHtml(q) + '…'; + + try { + const url = new URL(FUNCTION_URL); + url.searchParams.set('q', q); + + // Pass referring section as context if coming from a content page + const ref = document.referrer; + if (ref) { + try { + const refPath = new URL(ref).pathname.split('/').filter(Boolean); + if (refPath.length > 0) url.searchParams.set('context', refPath[0]); + } catch (_) {} + } + + const resp = await fetch(url.toString()); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + + // Hide spinner + statusEl.style.display = 'none'; + + // Show AI summary if available + if (data.summary?.summaryText) { + summaryTxt.textContent = data.summary.summaryText; + summaryEl.style.display = 'block'; + + // Render numbered citation list + const refs = data.summary?.citations || []; + const validRefs = refs.filter(r => r.title || r.uri); + + if (validRefs.length > 0) { + const citationHtml = validRefs.map((ref, i) => ` +
+ [${i + 1}] + ${ref.uri + ? `${escapeHtml(ref.title || ref.uri)}` + : `${escapeHtml(ref.title || 'Unknown source')}` + } +
+ `).join(''); + + const citationsDiv = document.createElement('div'); + citationsDiv.className = 'search-citations mt-3'; + citationsDiv.innerHTML = '

Sources

' + citationHtml; + summaryEl.querySelector('.ai-summary').appendChild(citationsDiv); + } + } + + // Show results + if (!data.results || data.results.length === 0) { + hitsEl.innerHTML = `

No results found for ${escapeHtml(q)}.

`; + return; + } + + // Show result count + hitsEl.innerHTML = + `

${data.results.length} results for ${escapeHtml(q)}

` + + data.results.map(r => ` +
+
+ ${escapeHtml(r.title || 'Untitled')} +
+ ${r.snippet ? `

${escapeHtml(r.snippet)}

` : ''} +

${escapeHtml(r.url)}

+
+ `).join(''); + + } catch (err) { + statusEl.textContent = 'Search error: ' + err.message; + console.error('Vertex search error:', err); + } + } + + // Run search on page load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => doSearch(query)); + } else { + doSearch(query); + } + +})(); \ No newline at end of file diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 7f05b07e..2e23b2fd 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -93,3 +93,86 @@ padding: 0.5rem 0; } } + +// Vertex AI Search styles +.ai-summary { + background: #f0f7ff; + border-left: 4px solid #1a73e8; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + border-radius: 0 4px 4px 0; + + .ai-badge { + font-size: 0.75rem; + font-weight: 600; + color: #1a73e8; + text-transform: uppercase; + letter-spacing: 0.05em; + display: block; + margin-bottom: 0.5rem; + } + + p { margin: 0; } +} + +.td-search-hit { + padding: 1rem 0; + border-bottom: 1px solid #e8e8e8; + + &:last-child { border-bottom: none; } + + &__title { + margin: 0 0 0.25rem; + font-size: 1rem; + a { color: #1a0dab; } + } + + &__snippet { + font-size: 0.875rem; + color: #545454; + margin: 0.25rem 0; + } + + &__url { + color: #006621; + margin: 0; + } +} + +.td-search-loading, +.td-search-no-results, +.td-search-error { + padding: 1rem 0; + color: #666; +} + +.search-citations { + border-top: 1px solid #d0e4ff; + padding-top: 0.75rem; + margin-top: 0.75rem; + + .citations-label { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #1a73e8; + margin-bottom: 0.4rem; + } + } + + .search-citation { + font-size: 0.8rem; + margin-bottom: 0.25rem; + + .citation-number { + color: #1a73e8; + font-weight: 600; + margin-right: 0.4rem; + } + + a { + color: #444; + &:hover { color: #1a73e8; } + } + } \ No newline at end of file diff --git a/config/_default/params.yaml b/config/_default/params.yaml index 0ccd9e4d..37d151ec 100644 --- a/config/_default/params.yaml +++ b/config/_default/params.yaml @@ -57,6 +57,15 @@ copyright: # gcs_engine_id: search engine gcs_engine_id: 33ef4cbe0703b4f3a +# Tell Docsy to show the search box but use offsite search +# (we intercept it with our own JS) +#offsite_search: +# enable: true +# custom_search_url: "https://us-central1-interlispsearch.cloudfunctions.net/search" + +# Vertex AI Search Cloud Function URL +vertex_search_url: "https://us-central1-interlispsearch.cloudfunctions.net/search" + # Zotero Group ID for bibliography management zotero_group_id: 2914042 diff --git a/layouts/_partials/hooks/head-end.html b/layouts/_partials/hooks/head-end.html index 9daa28c0..b7e49580 100644 --- a/layouts/_partials/hooks/head-end.html +++ b/layouts/_partials/hooks/head-end.html @@ -1,2 +1,5 @@ + +{{ $searchJS := resources.Get "js/vertex-search.js" | js.Build | fingerprint }} + diff --git a/layouts/search.html b/layouts/search.html new file mode 100644 index 00000000..4d917ab1 --- /dev/null +++ b/layouts/search.html @@ -0,0 +1,33 @@ +{{ define "main" }} +
+
+

Search Results

+ +
+ + {{/* Pass the function URL to JS */}} + + + + +
+ Searching... +
+ +
+ +
+
+
+{{ end }} \ No newline at end of file diff --git a/search-function/.gcloudignore b/search-function/.gcloudignore new file mode 100644 index 00000000..1c2b3137 --- /dev/null +++ b/search-function/.gcloudignore @@ -0,0 +1,3 @@ +node_modules/ +.git/ +*.md \ No newline at end of file diff --git a/search-function/index.js b/search-function/index.js new file mode 100644 index 00000000..4d85d98b --- /dev/null +++ b/search-function/index.js @@ -0,0 +1,163 @@ +const { GoogleAuth } = require('google-auth-library'); + +const PROJECT_ID = process.env.PROJECT_ID; +const ENGINE_ID = process.env.ENGINE_ID; +const LOCATION = 'global'; + +const auth = new GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/cloud-platform'] +}); + +exports.search = async (req, res) => { + + const allowedOrigins = [ + 'https://interlisp.org', + 'https://www.interlisp.org', + 'https://stumbo.github.io', + 'http://localhost:1313', + 'http://localhost:8080', + ]; + + const origin = req.headers.origin || ''; + const allowedOrigin = allowedOrigins.includes(origin) ? origin : 'https://interlisp.org'; + + res.set('Access-Control-Allow-Origin', allowedOrigin); + res.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.set('Access-Control-Allow-Headers', 'Content-Type'); + res.set('Access-Control-Max-Age', '3600'); + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + const query = req.query.q || req.body?.q || ''; + const context = req.query.context || req.body?.context || ''; + const pageSize = parseInt(req.query.pageSize) || 10; + + if (!query.trim()) { + res.status(400).json({ error: 'Missing query parameter q' }); + return; + } + + try { + // Use raw REST API to avoid SDK auto-pagination swallowing the summary + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + const endpoint = `https://discoveryengine.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/collections/default_collection/engines/${ENGINE_ID}/servingConfigs/default_config:search`; + + const requestBody = { + query, + pageSize, + contentSearchSpec: { + summarySpec: { + summaryResultCount: 5, + includeCitations: true, + useSemanticChunks: true, + languageCode: 'en-US', + modelPromptSpec: { + preamble: buildPreamble(context) + }, + modelSpec: { + version: 'stable' + } + }, + snippetSpec: { + returnSnippet: true + }, + extractiveContentSpec: { + maxExtractiveAnswerCount: 3 + } + } + }; + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token.token}`, + 'Content-Type': 'application/json', + 'x-goog-user-project': PROJECT_ID + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`Vertex API error ${response.status}: ${errText}`); + } + + const data = await response.json(); + + // Add this right after const data = await response.json(); + console.log('FIRST RESULT:', JSON.stringify(data.results?.[0], null, 2)); + console.log('SUMMARY FULL:', JSON.stringify(data.summary, null, 2)); + console.log('CITATIONS:', JSON.stringify(data.summary?.summaryWithMetadata?.references?.[0])); + + console.log('RESPONSE KEYS:', Object.keys(data)); + console.log('SUMMARY:', JSON.stringify(data.summary)); + console.log('RESULT COUNT:', (data.results || []).length); + + const results = (data.results || []).map(result => { + const derived = result.document?.derivedStructData; + if (!derived) return null; + + const url = derived.link || null; + const snippet = derived.snippets?.[0]?.snippet || null; + // Strip HTML tags from snippet for clean display + const stripHtml = (str) => str ? str.replace(/<[^>]*>/g, '') : null; + + return { + id: result.document?.id, + title: derived.title || null, + url, + snippet: stripHtml(derived.snippets?.[0]?.snippet || null), + section: url?.replace('https://interlisp.org/', '')?.split('/')?.[0] || '', + }; + }).filter(r => r?.url); + + // Build a map of document ID to URL from search results + const docIdToUrl = {}; + (data.results || []).forEach(result => { + const id = result.document?.id; + const url = result.document?.derivedStructData?.link; + if (id && url) docIdToUrl[id] = url; + }); + + // Enrich references with URLs by matching document IDs + const references = (data.summary?.summaryWithMetadata?.references || []).map(ref => { + // Extract document ID from the full document path + const docId = ref.document?.split('/').pop(); + return { + title: ref.title, + uri: docIdToUrl[docId] || null, + docId + }; + }); + + const summaryText = data.summary?.summaryText || null; + + res.json({ + summary: summaryText ? { + summaryText, + citations: references + } : null, + results + }); + + } catch (err) { + console.error('Search error:', err); + res.status(500).json({ error: 'Search failed', detail: err.message }); + } +}; + +function buildPreamble(context) { + const base = `You are a search assistant for the Interlisp documentation site. +Answer questions clearly and concisely. Always cite the sources you used. +If no relevant results exist, say so directly rather than guessing.`; + + if (context) { + return `${base}\nThe user is currently browsing the "${context}" section — prioritize results from that section where relevant.`; + } + return base; +} \ No newline at end of file diff --git a/search-function/package-lock.json b/search-function/package-lock.json new file mode 100644 index 00000000..54ddd3e8 --- /dev/null +++ b/search-function/package-lock.json @@ -0,0 +1,1368 @@ +{ + "name": "search-function", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "search-function", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@google-cloud/discoveryengine": "^2.6.0", + "@google-cloud/firestore": "^8.5.0", + "google-auth-library": "^10.6.2" + } + }, + "node_modules/@google-cloud/discoveryengine": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@google-cloud/discoveryengine/-/discoveryengine-2.6.0.tgz", + "integrity": "sha512-UP2hCCTW7R3rnTnILL4W+9fum2SaK96NomA8spINinPrKC3mxWrz6wtMkQcx8fTxZThkolNIN/pFgf4bTT2xUg==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-8.5.0.tgz", + "integrity": "sha512-1sbtj7JQfsfekrxwHR0s2CAz8+hkpBdiuB7+20VEgI4EWvW3+GhEUqYOQPRoZ2spHyITLN4gaOPmBN17J539yQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^5.0.1", + "protobufjs": "^7.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT" + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", + "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", + "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/teeny-request": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.2.tgz", + "integrity": "sha512-Xj0ZAQ0CeuQn6UxCDPLbFRlgcSTUEyO3+wiepr2grjIjyL/lMMs1Z4OwXn8kLvn/V1OuaEP0UY7Na6UDNNsYrQ==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/search-function/package.json b/search-function/package.json new file mode 100644 index 00000000..d6b41b19 --- /dev/null +++ b/search-function/package.json @@ -0,0 +1,18 @@ +{ + "name": "search-function", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@google-cloud/discoveryengine": "^2.6.0", + "@google-cloud/firestore": "^8.5.0", + "google-auth-library": "^10.6.2" + } +} diff --git a/search-function/rateLimiter.js b/search-function/rateLimiter.js new file mode 100644 index 00000000..02ed8f78 --- /dev/null +++ b/search-function/rateLimiter.js @@ -0,0 +1,101 @@ +'use strict'; + +const { Firestore } = require('@google-cloud/firestore'); + +const db = new Firestore({ projectId: process.env.PROJECT_ID }); + +// Rate limit configuration +// Adjust these values based on your traffic patterns +const LIMITS = { + perIp: { + requests: 20, // max 20 requests per IP + windowSec: 60, // per 60 second window + }, + global: { + requests: 500, // max 500 total requests + windowSec: 60, // per 60 second window + } +}; + +/** + * Atomically check and increment a rate limit counter in Firestore. + * Returns { allowed: true/false, count: current count } + */ +async function checkLimit(key, limit) { + const ref = db.collection('rate_limits').doc(key); + const now = Date.now(); + const windowMs = limit.windowSec * 1000; + + try { + const result = await db.runTransaction(async t => { + const doc = await t.get(ref); + const data = doc.exists ? doc.data() : null; + + // Start a new window if first request or window has expired + if (!data || (now - data.windowStart) > windowMs) { + t.set(ref, { + count: 1, + windowStart: now, + updatedAt: now + }); + return { allowed: true, count: 1 }; + } + + // Window is active — check if over limit + if (data.count >= limit.requests) { + return { allowed: false, count: data.count }; + } + + // Increment counter + t.update(ref, { + count: Firestore.FieldValue.increment(1), + updatedAt: now + }); + return { allowed: true, count: data.count + 1 }; + }); + + return result; + + } catch (err) { + // Fail open — if Firestore is unavailable don't block searches + console.error('Rate limiter error:', err.message); + return { allowed: true, count: 0 }; + } +} + +/** + * Main rate limit check — runs IP and global checks in parallel. + * Returns { limited: false } or { limited: true, reason, retryAfter } + */ +async function isRateLimited(req) { + // Get client IP — Cloud Functions passes real IP in x-forwarded-for + const ip = req.headers['x-forwarded-for'] + ?.split(',')[0] + ?.trim() || 'unknown'; + + // Run both checks in parallel + const [ipCheck, globalCheck] = await Promise.all([ + checkLimit(`ip:${ip}`, LIMITS.perIp), + checkLimit('global', LIMITS.global), + ]); + + if (!ipCheck.allowed) { + return { + limited: true, + reason: 'Too many requests. Please wait a moment before searching again.', + retryAfter: LIMITS.perIp.windowSec + }; + } + + if (!globalCheck.allowed) { + return { + limited: true, + reason: 'Search service is temporarily busy. Please try again shortly.', + retryAfter: LIMITS.global.windowSec + }; + } + + return { limited: false }; +} + +module.exports = { isRateLimited }; diff --git a/search-function/vertex-ai-search-implementation.md b/search-function/vertex-ai-search-implementation.md new file mode 100644 index 00000000..ef23ab11 --- /dev/null +++ b/search-function/vertex-ai-search-implementation.md @@ -0,0 +1,715 @@ +# Vertex AI Search Integration with Hugo (Docsy) +## Implementation Guide for Interlisp.org + +This document captures the complete steps taken to replace Google Custom Search (GCS) with a Vertex AI Search (Agent Search) powered search engine on a Hugo/Docsy site hosted on GitHub Pages. + +--- + +## Architecture Overview + +``` +Browser → Hugo Search Page → Cloud Function (proxy) → Vertex AI Search (Agent Search) + ↑ + Service Account (auth) + ↑ + Data Store (interlisp.org crawl) +``` + +### Components + +| Component | Name/ID | Purpose | +|---|---|---| +| GCP Project | `interlispsearch` (191169864763) | Container for all resources | +| Service Account | `vertex-search-sa@interlispsearch.iam.gserviceaccount.com` | Auth identity for Cloud Function | +| Data Store | `interlisp-site-search` | Crawled and indexed site content | +| Search Engine | `interlisp-site-search-v2_1777510147931` | Search app with LLM add-on | +| Cloud Function | `search` (us-central1) | Proxy between Hugo frontend and Vertex AI | + +--- + +## Part 1 — GCP Project Setup + +### 1.1 Enable Required APIs + +```bash +gcloud services enable discoveryengine.googleapis.com +gcloud services enable cloudbuild.googleapis.com +gcloud services enable storage.googleapis.com +gcloud services enable orgpolicy.googleapis.com +``` + +### 1.2 Create Service Account + +```bash +gcloud iam service-accounts create vertex-search-sa \ + --display-name="Vertex Search Service Account" \ + --project=interlispsearch +``` + +### 1.3 Grant Service Account Roles + +```bash +# Core role for Vertex AI Search access +gcloud projects add-iam-policy-binding interlispsearch \ + --member="serviceAccount:vertex-search-sa@interlispsearch.iam.gserviceaccount.com" \ + --role="roles/discoveryengine.editor" + +# Allow reading from Cloud Storage (for future JSONL imports) +gcloud projects add-iam-policy-binding interlispsearch \ + --member="serviceAccount:vertex-search-sa@interlispsearch.iam.gserviceaccount.com" \ + --role="roles/storage.objectViewer" +``` + +### 1.4 Override Org Policies (Project Level) + +The interlisp.org GCP organization had two restrictive org policies that blocked key creation and public function invocation. These were overridden at the project level without affecting the org-wide policy. + +```bash +# Allow allUsers IAM bindings in this project (needed for public Cloud Function) +cat > allow-all-users.yaml << 'EOF' +name: projects/interlispsearch/policies/iam.allowedPolicyMemberDomains +spec: + inheritFromParent: false + rules: + - allowAll: true +EOF + +gcloud org-policies set-policy allow-all-users.yaml --project=interlispsearch + +# Allow service account key creation in this project +cat > allow-sa-keys.yaml << 'EOF' +name: projects/interlispsearch/policies/iam.disableServiceAccountKeyCreation +spec: + inheritFromParent: false + rules: + - enforce: false +EOF + +gcloud org-policies set-policy allow-sa-keys.yaml --project=interlispsearch +``` + +### 1.5 Set Up Local Authentication + +```bash +# Authenticate with Application Default Credentials +gcloud auth application-default login + +# Set quota project (required for Discovery Engine API calls) +gcloud auth application-default set-quota-project interlispsearch + +# Set default project +gcloud config set project interlispsearch +``` + +**Important:** All `curl` calls to the Discovery Engine API require the `x-goog-user-project` header: + +```bash +-H "x-goog-user-project: interlispsearch" +``` + +--- + +## Part 2 — Vertex AI Data Store + +### 2.1 Create the Data Store + +```bash +TOKEN=$(gcloud auth application-default print-access-token) + +curl -X POST \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -H "x-goog-user-project: interlispsearch" \ + "https://discoveryengine.googleapis.com/v1/projects/interlispsearch/locations/global/collections/default_collection/dataStores?dataStoreId=interlisp-site-search" \ + -d '{ + "displayName": "Interlisp Site Search", + "industryVertical": "GENERIC", + "solutionTypes": ["SOLUTION_TYPE_SEARCH"], + "contentConfig": "PUBLIC_WEBSITE" + }' +``` + +**Key parameters:** +- `contentConfig: PUBLIC_WEBSITE` — enables website crawling +- `industryVertical: GENERIC` — required for non-Workspace data stores +- Location must be `global` for AI summarization features + +### 2.2 Add Target Site + +The URL pattern does **not** include the `https://` protocol prefix: + +```bash +TOKEN=$(gcloud auth application-default print-access-token) + +curl -X POST \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -H "x-goog-user-project: interlispsearch" \ + "https://discoveryengine.googleapis.com/v1/projects/interlispsearch/locations/global/collections/default_collection/dataStores/interlisp-site-search/siteSearchEngine/targetSites" \ + -d '{ + "providedUriPattern": "interlisp.org/*", + "type": "INCLUDE", + "exactMatch": false + }' +``` + +### 2.3 Upgrade to Advanced Website Indexing + +Advanced indexing is required for AI summarization features. This is done in the GCP console: + +1. Go to **AI Applications → Data Stores → interlisp-site-search** +2. Click the **Data** tab +3. Click **Upgrade to Advanced** next to the URL pattern + +Upgrading takes 4-8 hours for a site the size of interlisp.org. Check status: + +```bash +TOKEN=$(gcloud auth application-default print-access-token) + +curl -H "Authorization: Bearer $TOKEN" \ + -H "x-goog-user-project: interlispsearch" \ + "https://discoveryengine.googleapis.com/v1/projects/interlispsearch/locations/global/collections/default_collection/dataStores/interlisp-site-search/siteSearchEngine/targetSites" \ + | python3 -m json.tool +``` + +Look for `indexingStatus: SUCCEEDED` to confirm completion. + +--- + +## Part 3 — Vertex AI Search Engine (App) + +### 3.1 Important: App Type Selection + +The search engine must be created as **"Site search with AI mode"** in the console — NOT as "Gemini Enterprise" or "Custom Search (general)". + +- **Gemini Enterprise** — for internal workspace knowledge bases, incompatible with website data stores +- **Custom Search (general)** — for structured document/JSONL data, not website crawling +- **Site search with AI mode** ✓ — correct type for public website crawling with AI summaries + +### 3.2 Create via Console + +1. Go to **AI Applications → Apps → Create App** +2. Select **Search → Site search with AI mode** +3. Fill in details: + - App name: `interlisp-site-search-v2` + - Company name: `interlisp.org` + - Location: `global` +4. Select data store: `interlisp-site-search` +5. Note the generated engine ID: `interlisp-site-search-v2_1777510147931` + +### 3.3 Verify Engine Configuration + +```bash +TOKEN=$(gcloud auth application-default print-access-token) + +curl -H "Authorization: Bearer $TOKEN" \ + -H "x-goog-user-project: interlispsearch" \ + "https://discoveryengine.googleapis.com/v1/projects/interlispsearch/locations/global/collections/default_collection/engines/interlisp-site-search-v2_1777510147931" \ + | python3 -m json.tool +``` + +Confirm the response contains: +```json +{ + "searchEngineConfig": { + "searchTier": "SEARCH_TIER_ENTERPRISE", + "searchAddOns": ["SEARCH_ADD_ON_LLM"] + } +} +``` + +`SEARCH_ADD_ON_LLM` is required for AI summaries. + +--- + +## Part 4 — Cloud Function (Search Proxy) + +The Cloud Function acts as a secure proxy between the public Hugo frontend and the authenticated Vertex AI Search API. It also builds the AI prompt context. + +### 4.1 Project Structure + +``` +hugo-site/ +└── search-function/ + ├── index.js + ├── package.json + └── .gcloudignore +``` + +### 4.2 `package.json` + +```json +{ + "name": "search-function", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@google-cloud/discoveryengine": "^1.0.0", + "google-auth-library": "^9.0.0" + } +} +``` + +### 4.3 `index.js` + +The function uses the raw REST API (not the Node.js SDK) to avoid SDK auto-pagination which strips the `summary` field from responses. + +```javascript +const { GoogleAuth } = require('google-auth-library'); + +const PROJECT_ID = process.env.PROJECT_ID; +const ENGINE_ID = process.env.ENGINE_ID; +const LOCATION = 'global'; + +const auth = new GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/cloud-platform'] +}); + +exports.search = async (req, res) => { + + // CORS — allow specific origins only + const allowedOrigins = [ + 'https://interlisp.org', + 'https://www.interlisp.org', + 'https://stumbo.github.io', + 'http://localhost:1313', + 'http://localhost:8080', + ]; + + const origin = req.headers.origin || ''; + const allowedOrigin = allowedOrigins.includes(origin) ? origin : 'https://interlisp.org'; + + res.set('Access-Control-Allow-Origin', allowedOrigin); + res.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.set('Access-Control-Allow-Headers', 'Content-Type'); + res.set('Access-Control-Max-Age', '3600'); + + if (req.method === 'OPTIONS') { + res.status(204).send(''); + return; + } + + const query = req.query.q || req.body?.q || ''; + const context = req.query.context || req.body?.context || ''; + const pageSize = parseInt(req.query.pageSize) || 10; + + if (!query.trim()) { + res.status(400).json({ error: 'Missing query parameter q' }); + return; + } + + try { + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + const endpoint = `https://discoveryengine.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/collections/default_collection/engines/${ENGINE_ID}/servingConfigs/default_config:search`; + + const requestBody = { + query, + pageSize, + contentSearchSpec: { + summarySpec: { + summaryResultCount: 5, + includeCitations: true, + modelPromptSpec: { + preamble: buildPreamble(context) + } + }, + snippetSpec: { + returnSnippet: true + }, + extractiveContentSpec: { + maxExtractiveAnswerCount: 3 + } + } + }; + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token.token}`, + 'Content-Type': 'application/json', + 'x-goog-user-project': PROJECT_ID + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`Vertex API error ${response.status}: ${errText}`); + } + + const data = await response.json(); + + // REST API returns derivedStructData as flat object (not nested under .fields) + const results = (data.results || []).map(result => { + const derived = result.document?.derivedStructData; + if (!derived) return null; + + const url = derived.link || null; + const snippet = derived.snippets?.[0]?.snippet || null; + + return { + id: result.document?.id, + title: derived.title || null, + url, + snippet, + section: url?.replace('https://interlisp.org/', '')?.split('/')?.[0] || '', + }; + }).filter(r => r?.url); + + const summaryText = data.summary?.summaryText || null; + + res.json({ + summary: summaryText ? { + summaryText, + citations: data.summary?.summaryWithMetadata?.references || [] + } : null, + results + }); + + } catch (err) { + console.error('Search error:', err); + res.status(500).json({ error: 'Search failed', detail: err.message }); + } +}; + +function buildPreamble(context) { + const base = `You are a search assistant for the Interlisp documentation site. +Answer questions clearly and concisely. Always cite the sources you used. +If no relevant results exist, say so directly rather than guessing.`; + + if (context) { + return `${base}\nThe user is currently browsing the "${context}" section — prioritize results from that section where relevant.`; + } + return base; +} +``` + +### 4.4 `.gcloudignore` + +``` +node_modules/ +.git/ +*.md +``` + +### 4.5 Deploy + +```bash +cd search-function +npm install + +gcloud functions deploy search \ + --gen2 \ + --runtime=nodejs24 \ + --region=us-central1 \ + --source=. \ + --entry-point=search \ + --trigger-http \ + --allow-unauthenticated \ + --service-account=vertex-search-sa@interlispsearch.iam.gserviceaccount.com \ + --set-env-vars PROJECT_ID=interlispsearch,ENGINE_ID=interlisp-site-search-v2_1777510147931,ALLOWED_ORIGIN=https://interlisp.org \ + --memory=256Mi \ + --timeout=30s \ + --project=interlispsearch +``` + +**Key deployment decisions:** +- `--gen2` — required for Cloud Run-based functions with better performance +- `--allow-unauthenticated` — public endpoint, CORS restricts access by domain in function code +- `--service-account` — function runs as the service account, which has Discovery Engine access +- No key file needed — the service account is attached at deploy time + +### 4.6 Verify Deployment + +```bash +# Test without auth token (should return results) +curl -s "https://us-central1-interlispsearch.cloudfunctions.net/search?q=interlisp" \ + | python3 -m json.tool | head -30 +``` + +--- + +## Part 5 — Hugo / Docsy Integration + +The site uses Hugo with the **Docsy** theme (v0.14.3) loaded as a Hugo module. Docsy renders its search box only when `gcs_engine_id` is set in params — this param is kept to preserve the search UI, while the results page is completely overridden. + +### 5.1 How Docsy Search Works + +When a user types in the search box and presses Enter, Docsy's `search.js` redirects to `/search/?q=query`. The results page (`layouts/search.html`) is where we intercept and replace GCS with Vertex AI. + +### 5.2 `config/_default/params.yaml` Changes + +```yaml +# Keep this — Docsy won't render the search box without it +gcs_engine_id: 33ef4cbe0703b4f3a + +# Add Vertex AI Search Cloud Function URL +vertex_search_url: "https://us-central1-interlispsearch.cloudfunctions.net/search" +``` + +### 5.3 Override Search Results Page + +Create `layouts/search.html` (overrides Docsy's GCS results page): + +```html +{{ define "main" }} +
+
+

Search Results

+ +
+ + + + + +
+ Searching... +
+ +
+ +
+
+
+{{ end }} +``` + +### 5.4 Search Widget JavaScript + +Create `assets/js/vertex-search.js`: + +```javascript +(function () { + 'use strict'; + + const config = document.getElementById('vertex-search-config'); + if (!config) return; + + const FUNCTION_URL = config.dataset.searchUrl; + if (!FUNCTION_URL) return; + + const urlParams = new URLSearchParams(window.location.search); + const query = urlParams.get('q') || ''; + + const statusEl = document.getElementById('vertex-search-status'); + const hitsEl = document.getElementById('vertex-search-hits'); + const summaryEl = document.getElementById('vertex-search-summary'); + const summaryTxt = document.getElementById('summary-text'); + + function escapeHtml(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + async function doSearch(q) { + if (!q) { + statusEl.textContent = 'Enter a search query above.'; + return; + } + + document.title = `Search: ${q}`; + statusEl.innerHTML = ' Searching for ' + + escapeHtml(q) + '…'; + + try { + const url = new URL(FUNCTION_URL); + url.searchParams.set('q', q); + + // Pass referring section as context + const ref = document.referrer; + if (ref) { + try { + const refPath = new URL(ref).pathname.split('/').filter(Boolean); + if (refPath.length > 0) url.searchParams.set('context', refPath[0]); + } catch (_) {} + } + + const resp = await fetch(url.toString()); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + + statusEl.style.display = 'none'; + + // Show AI summary if available + if (data.summary?.summaryText) { + summaryTxt.textContent = data.summary.summaryText; + summaryEl.style.display = 'block'; + } + + if (!data.results || data.results.length === 0) { + hitsEl.innerHTML = `

No results found for ${escapeHtml(q)}.

`; + return; + } + + hitsEl.innerHTML = + `

${data.results.length} results for ${escapeHtml(q)}

` + + data.results.map(r => ` +
+
+ ${escapeHtml(r.title || 'Untitled')} +
+ ${r.snippet ? `

${escapeHtml(r.snippet)}

` : ''} +

${escapeHtml(r.url)}

+
+ `).join(''); + + } catch (err) { + statusEl.textContent = 'Search error: ' + err.message; + console.error('Vertex search error:', err); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => doSearch(query)); + } else { + doSearch(query); + } + +})(); +``` + +### 5.5 Load JS and Pass Config via `head-end.html` + +Update `layouts/_partials/hooks/head-end.html`: + +```html + + + +{{ $searchJS := resources.Get "js/vertex-search.js" | js.Build | fingerprint }} + +``` + +### 5.6 Add Styles + +Add to `assets/scss/_styles_project.scss`: + +```scss +// Vertex AI Search styles +.ai-summary { + background: #f0f7ff; + border-left: 4px solid #1a73e8; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + border-radius: 0 4px 4px 0; + + .ai-badge { + font-size: 0.75rem; + font-weight: 600; + color: #1a73e8; + text-transform: uppercase; + letter-spacing: 0.05em; + display: block; + margin-bottom: 0.5rem; + } + + p { margin: 0; } +} + +.td-search-hit { + padding: 1rem 0; + border-bottom: 1px solid #e8e8e8; + + &:last-child { border-bottom: none; } + + &__title { + margin: 0 0 0.25rem; + font-size: 1rem; + a { color: #1a0dab; } + } + + &__snippet { + font-size: 0.875rem; + color: #545454; + margin: 0.25rem 0; + } + + &__url { + color: #006621; + margin: 0; + } +} +``` + +--- + +## Part 6 — File Summary + +### New Files Created + +| File | Purpose | +|---|---| +| `search-function/index.js` | Cloud Function — proxy to Vertex AI Search | +| `search-function/package.json` | Node.js dependencies | +| `search-function/.gcloudignore` | Excludes node_modules from deploy | +| `layouts/search.html` | Overrides Docsy's GCS results page | +| `assets/js/vertex-search.js` | Frontend search widget JS | + +### Modified Files + +| File | Change | +|---|---| +| `config/_default/params.yaml` | Added `vertex_search_url`, kept `gcs_engine_id` | +| `layouts/_partials/hooks/head-end.html` | Added JS bundle load | +| `assets/scss/_styles_project.scss` | Added search result styles | + +--- + +## Part 7 — Key Lessons Learned + +### Authentication +- Local `curl` testing requires the `x-goog-user-project: interlispsearch` header with ADC credentials — without it, requests are billed against a Google-internal project and fail with 403 +- Cloud Functions authenticate via the attached service account at runtime — no key file needed +- Browser JS cannot call authenticated Cloud Functions directly — the function must be public (`--allow-unauthenticated`) with CORS restricting by origin in code + +### Vertex AI SDK vs REST API +- The Node.js `@google-cloud/discoveryengine` SDK uses auto-pagination by default, which flattens the response into a plain array of results and **strips the `summary` field** +- Using the raw REST API via `fetch` preserves the full response structure including `summary`, `totalSize`, `attributionToken`, and `semanticState` +- The REST API also returns `derivedStructData` as a flat object (e.g. `derived.title`) rather than nested under `fields` as the SDK does (e.g. `fields.title.stringValue`) + +### Org Policies +- `constraints/iam.allowedPolicyMemberDomains` blocked `allUsers` invocation +- `constraints/iam.disableServiceAccountKeyCreation` blocked service account key files +- Both were overridden at the project level using `gcloud org-policies set-policy` — requires `roles/owner` on the project but not org-level admin access + +### Docsy Search Integration +- Docsy only renders the search box when `gcs_engine_id` is set in params +- The search box redirects to `/search/?q=query` on Enter — this is handled by Docsy's own `search.js` +- Override `layouts/search.html` to replace GCS results with Vertex AI results +- Do not try to intercept the search input directly — work with Docsy's redirect behavior + +### Data Store Type +- Must be created as **"Site search with AI mode"** in the AI Applications console +- **"Gemini Enterprise"** app type is incompatible with website data stores +- URL patterns for target sites must **not** include `https://` protocol prefix +- Advanced website indexing (upgrade from Basic) is required for AI summarization — takes 4-8 hours + +--- + +## Part 8 — Pending Items + +- [ ] Confirm Advanced indexing completes (`indexingStatus: SUCCEEDED`) +- [ ] Verify AI summary appears in search results once indexing is complete +- [ ] Deploy to staging site (`stumbo.github.io/InterlispDraft.github.io`) +- [ ] Test CORS from staging domain +- [ ] Deploy to production (`interlisp.org`) +- [ ] Set up CI/CD to re-index when site content changes (JSONL approach) +- [ ] Remove debug `console.log` statements from `index.js` +- [ ] Consider rate limiting on the Cloud Function for production From 7d3cfc55b5ed0e3d4513a2707c1e6aedce7ecacd Mon Sep 17 00:00:00 2001 From: Bill Stumbo Date: Sun, 3 May 2026 23:12:03 -0400 Subject: [PATCH 2/4] Update implementation document with firebase work, minor cleanup --- config/_default/params.yaml | 6 - search-function/index.js | 4 +- .../vertex-ai-search-implementation.md | 364 +++++++++++++++--- 3 files changed, 315 insertions(+), 59 deletions(-) diff --git a/config/_default/params.yaml b/config/_default/params.yaml index 37d151ec..90adbd39 100644 --- a/config/_default/params.yaml +++ b/config/_default/params.yaml @@ -57,12 +57,6 @@ copyright: # gcs_engine_id: search engine gcs_engine_id: 33ef4cbe0703b4f3a -# Tell Docsy to show the search box but use offsite search -# (we intercept it with our own JS) -#offsite_search: -# enable: true -# custom_search_url: "https://us-central1-interlispsearch.cloudfunctions.net/search" - # Vertex AI Search Cloud Function URL vertex_search_url: "https://us-central1-interlispsearch.cloudfunctions.net/search" diff --git a/search-function/index.js b/search-function/index.js index 4d85d98b..5d0c90ae 100644 --- a/search-function/index.js +++ b/search-function/index.js @@ -104,8 +104,8 @@ exports.search = async (req, res) => { const url = derived.link || null; const snippet = derived.snippets?.[0]?.snippet || null; - // Strip HTML tags from snippet for clean display - const stripHtml = (str) => str ? str.replace(/<[^>]*>/g, '') : null; + // Strip angle brackets to prevent HTML/script tag injection in display text + const stripHtml = (str) => str ? str.replace(/[<>]/g, '') : null; return { id: result.document?.id, diff --git a/search-function/vertex-ai-search-implementation.md b/search-function/vertex-ai-search-implementation.md index ef23ab11..d82f7ca7 100644 --- a/search-function/vertex-ai-search-implementation.md +++ b/search-function/vertex-ai-search-implementation.md @@ -9,8 +9,8 @@ This document captures the complete steps taken to replace Google Custom Search ``` Browser → Hugo Search Page → Cloud Function (proxy) → Vertex AI Search (Agent Search) - ↑ - Service Account (auth) + ↑ ↑ + Service Account Firestore (rate limiting) ↑ Data Store (interlisp.org crawl) ``` @@ -24,6 +24,7 @@ Browser → Hugo Search Page → Cloud Function (proxy) → Vertex AI Search (Ag | Data Store | `interlisp-site-search` | Crawled and indexed site content | | Search Engine | `interlisp-site-search-v2_1777510147931` | Search app with LLM add-on | | Cloud Function | `search` (us-central1) | Proxy between Hugo frontend and Vertex AI | +| Firestore | `rate_limits` collection | Per-IP and global request rate limiting | --- @@ -36,6 +37,7 @@ gcloud services enable discoveryengine.googleapis.com gcloud services enable cloudbuild.googleapis.com gcloud services enable storage.googleapis.com gcloud services enable orgpolicy.googleapis.com +gcloud services enable firestore.googleapis.com ``` ### 1.2 Create Service Account @@ -58,6 +60,11 @@ gcloud projects add-iam-policy-binding interlispsearch \ gcloud projects add-iam-policy-binding interlispsearch \ --member="serviceAccount:vertex-search-sa@interlispsearch.iam.gserviceaccount.com" \ --role="roles/storage.objectViewer" + +# Allow Firestore read/write for rate limiting +gcloud projects add-iam-policy-binding interlispsearch \ + --member="serviceAccount:vertex-search-sa@interlispsearch.iam.gserviceaccount.com" \ + --role="roles/datastore.user" ``` ### 1.4 Override Org Policies (Project Level) @@ -232,6 +239,7 @@ The Cloud Function acts as a secure proxy between the public Hugo frontend and t hugo-site/ └── search-function/ ├── index.js + ├── rateLimiter.js ├── package.json └── .gcloudignore ``` @@ -245,6 +253,7 @@ hugo-site/ "main": "index.js", "dependencies": { "@google-cloud/discoveryengine": "^1.0.0", + "@google-cloud/firestore": "^7.0.0", "google-auth-library": "^9.0.0" } } @@ -252,10 +261,13 @@ hugo-site/ ### 4.3 `index.js` -The function uses the raw REST API (not the Node.js SDK) to avoid SDK auto-pagination which strips the `summary` field from responses. +The function uses the raw REST API (not the Node.js SDK) to avoid SDK auto-pagination which strips the `summary` field from responses. It includes Firestore-based rate limiting and resolves citation URLs by matching document IDs from results. ```javascript -const { GoogleAuth } = require('google-auth-library'); +'use strict'; + +const { GoogleAuth } = require('google-auth-library'); +const { isRateLimited } = require('./rateLimiter'); const PROJECT_ID = process.env.PROJECT_ID; const ENGINE_ID = process.env.ENGINE_ID; @@ -267,7 +279,6 @@ const auth = new GoogleAuth({ exports.search = async (req, res) => { - // CORS — allow specific origins only const allowedOrigins = [ 'https://interlisp.org', 'https://www.interlisp.org', @@ -276,19 +287,33 @@ exports.search = async (req, res) => { 'http://localhost:8080', ]; - const origin = req.headers.origin || ''; - const allowedOrigin = allowedOrigins.includes(origin) ? origin : 'https://interlisp.org'; + const origin = req.headers.origin || ''; + const allowedOrigin = allowedOrigins.includes(origin) + ? origin + : 'https://interlisp.org'; - res.set('Access-Control-Allow-Origin', allowedOrigin); + res.set('Access-Control-Allow-Origin', allowedOrigin); res.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); res.set('Access-Control-Allow-Headers', 'Content-Type'); - res.set('Access-Control-Max-Age', '3600'); + res.set('Access-Control-Max-Age', '3600'); if (req.method === 'OPTIONS') { res.status(204).send(''); return; } + // Rate limiting + const rateLimitResult = await isRateLimited(req); + if (rateLimitResult.limited) { + res.set('Retry-After', String(rateLimitResult.retryAfter)); + res.status(429).json({ + error: 'Rate limit exceeded', + message: rateLimitResult.reason, + retryAfter: rateLimitResult.retryAfter + }); + return; + } + const query = req.query.q || req.body?.q || ''; const context = req.query.context || req.body?.context || ''; const pageSize = parseInt(req.query.pageSize) || 10; @@ -310,9 +335,14 @@ exports.search = async (req, res) => { contentSearchSpec: { summarySpec: { summaryResultCount: 5, - includeCitations: true, + includeCitations: true, + useSemanticChunks: true, + languageCode: 'en-US', modelPromptSpec: { preamble: buildPreamble(context) + }, + modelSpec: { + version: 'stable' } }, snippetSpec: { @@ -325,10 +355,10 @@ exports.search = async (req, res) => { }; const response = await fetch(endpoint, { - method: 'POST', + method: 'POST', headers: { - 'Authorization': `Bearer ${token.token}`, - 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token.token}`, + 'Content-Type': 'application/json', 'x-goog-user-project': PROJECT_ID }, body: JSON.stringify(requestBody) @@ -341,20 +371,41 @@ exports.search = async (req, res) => { const data = await response.json(); + // Strip HTML tags from snippet text + const stripHtml = str => str ? str.replace(/<[^>]*>/g, '') : null; + + // Build document ID → URL map for citation linking + const docIdToUrl = {}; + (data.results || []).forEach(result => { + const id = result.document?.id; + const url = result.document?.derivedStructData?.link; + if (id && url) docIdToUrl[id] = url; + }); + + // Map references to include resolved URLs by matching document IDs + const references = (data.summary?.summaryWithMetadata?.references || []) + .map(ref => { + const docId = ref.document?.split('/').pop(); + return { + title: ref.title, + uri: docIdToUrl[docId] || null, + }; + }); + // REST API returns derivedStructData as flat object (not nested under .fields) const results = (data.results || []).map(result => { const derived = result.document?.derivedStructData; if (!derived) return null; - const url = derived.link || null; - const snippet = derived.snippets?.[0]?.snippet || null; + const url = derived.link || null; return { id: result.document?.id, title: derived.title || null, url, - snippet, - section: url?.replace('https://interlisp.org/', '')?.split('/')?.[0] || '', + snippet: stripHtml(derived.snippets?.[0]?.snippet || null), + section: url?.replace('https://interlisp.org/', '') + ?.split('/')?.[0] || '', }; }).filter(r => r?.url); @@ -363,7 +414,7 @@ exports.search = async (req, res) => { res.json({ summary: summaryText ? { summaryText, - citations: data.summary?.summaryWithMetadata?.references || [] + citations: references } : null, results }); @@ -418,7 +469,7 @@ gcloud functions deploy search \ **Key deployment decisions:** - `--gen2` — required for Cloud Run-based functions with better performance - `--allow-unauthenticated` — public endpoint, CORS restricts access by domain in function code -- `--service-account` — function runs as the service account, which has Discovery Engine access +- `--service-account` — function runs as the service account, which has Discovery Engine and Firestore access - No key file needed — the service account is attached at deploy time ### 4.6 Verify Deployment @@ -431,15 +482,164 @@ curl -s "https://us-central1-interlispsearch.cloudfunctions.net/search?q=interli --- -## Part 5 — Hugo / Docsy Integration +## Part 5 — Firestore Rate Limiting + +Rate limiting is implemented using Firestore to track request counts across all function instances. In-memory counters cannot be used because Cloud Functions can scale to multiple instances. + +### 5.1 Create the Firestore Database + +```bash +gcloud firestore databases create \ + --location=us-central1 \ + --project=interlispsearch +``` + +### 5.2 Set Up TTL to Auto-Clean Expired Documents + +This prevents the `rate_limits` collection from growing indefinitely: + +```bash +gcloud firestore fields ttls update updatedAt \ + --collection-group=rate_limits \ + --enable-ttl \ + --project=interlispsearch +``` + +This operation takes 5-15 minutes to propagate. Check status: + +```bash +gcloud firestore fields describe updatedAt \ + --collection-group=rate_limits \ + --project=interlispsearch +``` + +Look for `ttlConfig.state: ACTIVE` to confirm completion. + +### 5.3 `rateLimiter.js` + +Create `search-function/rateLimiter.js`: + +```javascript +'use strict'; + +const { Firestore } = require('@google-cloud/firestore'); + +const db = new Firestore({ projectId: process.env.PROJECT_ID }); + +const LIMITS = { + perIp: { + requests: 20, // max 20 requests per IP + windowSec: 60, // per 60 second window + }, + global: { + requests: 500, // max 500 total requests + windowSec: 60, // per 60 second window + } +}; + +async function checkLimit(key, limit) { + const ref = db.collection('rate_limits').doc(key); + const now = Date.now(); + const windowMs = limit.windowSec * 1000; + + try { + const result = await db.runTransaction(async t => { + const doc = await t.get(ref); + const data = doc.exists ? doc.data() : null; + + if (!data || (now - data.windowStart) > windowMs) { + t.set(ref, { count: 1, windowStart: now, updatedAt: now }); + return { allowed: true, count: 1 }; + } + + if (data.count >= limit.requests) { + return { allowed: false, count: data.count }; + } + + t.update(ref, { + count: Firestore.FieldValue.increment(1), + updatedAt: now + }); + return { allowed: true, count: data.count + 1 }; + }); + + return result; + + } catch (err) { + // Fail open — don't block searches if Firestore is unavailable + console.error('Rate limiter error:', err.message); + return { allowed: true, count: 0 }; + } +} + +async function isRateLimited(req) { + const ip = req.headers['x-forwarded-for'] + ?.split(',')[0]?.trim() || 'unknown'; + + const [ipCheck, globalCheck] = await Promise.all([ + checkLimit(`ip:${ip}`, LIMITS.perIp), + checkLimit('global', LIMITS.global), + ]); + + if (!ipCheck.allowed) { + return { + limited: true, + reason: 'Too many requests. Please wait a moment before searching again.', + retryAfter: LIMITS.perIp.windowSec + }; + } + + if (!globalCheck.allowed) { + return { + limited: true, + reason: 'Search service is temporarily busy. Please try again shortly.', + retryAfter: LIMITS.global.windowSec + }; + } + + return { limited: false }; +} + +module.exports = { isRateLimited }; +``` + +### 5.4 Rate Limit Configuration + +| Limit | Value | Notes | +|---|---|---| +| Per IP | 20 requests / 60 seconds | Prevents individual abuse | +| Global | 500 requests / 60 seconds | Protects against aggregate overload | +| Fail behavior | Open (allow) | If Firestore unavailable, searches proceed | + +### 5.5 Verify Rate Limiting + +```bash +# Check Firestore documents are created after searches +gcloud firestore documents list \ + --collection=rate_limits \ + --project=interlispsearch + +# Test that 429 is returned after limit is exceeded +for i in {1..25}; do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + "https://us-central1-interlispsearch.cloudfunctions.net/search?q=test") + echo "Request $i: $STATUS" +done +``` + +Requests 1-20 return `200`, requests 21+ return `429`. + +--- + +## Part 6 — Hugo / Docsy Integration The site uses Hugo with the **Docsy** theme (v0.14.3) loaded as a Hugo module. Docsy renders its search box only when `gcs_engine_id` is set in params — this param is kept to preserve the search UI, while the results page is completely overridden. -### 5.1 How Docsy Search Works +### 6.1 How Docsy Search Works When a user types in the search box and presses Enter, Docsy's `search.js` redirects to `/search/?q=query`. The results page (`layouts/search.html`) is where we intercept and replace GCS with Vertex AI. -### 5.2 `config/_default/params.yaml` Changes +### 6.2 `config/_default/params.yaml` Changes ```yaml # Keep this — Docsy won't render the search box without it @@ -449,7 +649,7 @@ gcs_engine_id: 33ef4cbe0703b4f3a vertex_search_url: "https://us-central1-interlispsearch.cloudfunctions.net/search" ``` -### 5.3 Override Search Results Page +### 6.3 Override Search Results Page Create `layouts/search.html` (overrides Docsy's GCS results page): @@ -487,7 +687,7 @@ Create `layouts/search.html` (overrides Docsy's GCS results page): {{ end }} ``` -### 5.4 Search Widget JavaScript +### 6.4 Search Widget JavaScript Create `assets/js/vertex-search.js`: @@ -547,10 +747,28 @@ Create `assets/js/vertex-search.js`: statusEl.style.display = 'none'; - // Show AI summary if available + // Show AI summary and citations if available if (data.summary?.summaryText) { summaryTxt.textContent = data.summary.summaryText; summaryEl.style.display = 'block'; + + const refs = data.summary?.citations || []; + const validRefs = refs.filter(r => r.title || r.uri); + if (validRefs.length > 0) { + const citationHtml = validRefs.map((ref, i) => ` +
+ [${i + 1}] + ${ref.uri + ? `${escapeHtml(ref.title || ref.uri)}` + : `${escapeHtml(ref.title || 'Unknown source')}` + } +
+ `).join(''); + const citationsDiv = document.createElement('div'); + citationsDiv.className = 'search-citations mt-3'; + citationsDiv.innerHTML = '

Sources

' + citationHtml; + summaryEl.querySelector('.ai-summary').appendChild(citationsDiv); + } } if (!data.results || data.results.length === 0) { @@ -585,7 +803,7 @@ Create `assets/js/vertex-search.js`: })(); ``` -### 5.5 Load JS and Pass Config via `head-end.html` +### 6.5 Load JS via `head-end.html` Update `layouts/_partials/hooks/head-end.html`: @@ -597,7 +815,7 @@ Update `layouts/_partials/hooks/head-end.html`: ``` -### 5.6 Add Styles +### 6.6 Add Styles Add to `assets/scss/_styles_project.scss`: @@ -628,35 +846,47 @@ Add to `assets/scss/_styles_project.scss`: border-bottom: 1px solid #e8e8e8; &:last-child { border-bottom: none; } +} - &__title { - margin: 0 0 0.25rem; - font-size: 1rem; - a { color: #1a0dab; } - } +.search-citations { + border-top: 1px solid #d0e4ff; + padding-top: 0.75rem; + margin-top: 0.75rem; - &__snippet { - font-size: 0.875rem; - color: #545454; - margin: 0.25rem 0; + .citations-label { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #1a73e8; + margin-bottom: 0.4rem; } +} + +.search-citation { + font-size: 0.8rem; + margin-bottom: 0.25rem; - &__url { - color: #006621; - margin: 0; + .citation-number { + color: #1a73e8; + font-weight: 600; + margin-right: 0.4rem; } + + a { color: #444; } } ``` --- -## Part 6 — File Summary +## Part 7 — File Summary ### New Files Created | File | Purpose | |---|---| | `search-function/index.js` | Cloud Function — proxy to Vertex AI Search | +| `search-function/rateLimiter.js` | Firestore-based rate limiting | | `search-function/package.json` | Node.js dependencies | | `search-function/.gcloudignore` | Excludes node_modules from deploy | | `layouts/search.html` | Overrides Docsy's GCS results page | @@ -668,11 +898,11 @@ Add to `assets/scss/_styles_project.scss`: |---|---| | `config/_default/params.yaml` | Added `vertex_search_url`, kept `gcs_engine_id` | | `layouts/_partials/hooks/head-end.html` | Added JS bundle load | -| `assets/scss/_styles_project.scss` | Added search result styles | +| `assets/scss/_styles_project.scss` | Added search result and citation styles | --- -## Part 7 — Key Lessons Learned +## Part 8 — Key Lessons Learned ### Authentication - Local `curl` testing requires the `x-goog-user-project: interlispsearch` header with ADC credentials — without it, requests are billed against a Google-internal project and fail with 403 @@ -682,7 +912,12 @@ Add to `assets/scss/_styles_project.scss`: ### Vertex AI SDK vs REST API - The Node.js `@google-cloud/discoveryengine` SDK uses auto-pagination by default, which flattens the response into a plain array of results and **strips the `summary` field** - Using the raw REST API via `fetch` preserves the full response structure including `summary`, `totalSize`, `attributionToken`, and `semanticState` -- The REST API also returns `derivedStructData` as a flat object (e.g. `derived.title`) rather than nested under `fields` as the SDK does (e.g. `fields.title.stringValue`) +- The REST API returns `derivedStructData` as a flat object (e.g. `derived.title`) rather than nested under `fields` as the SDK does (e.g. `fields.title.stringValue`) + +### Citations +- The `references` array in `summaryWithMetadata` contains document paths, not URLs +- URLs must be resolved by cross-referencing document IDs from the `results` array against the document path suffix in each reference +- `useSemanticChunks: true` in `summarySpec` improves citation accuracy ### Org Policies - `constraints/iam.allowedPolicyMemberDomains` blocked `allUsers` invocation @@ -701,15 +936,42 @@ Add to `assets/scss/_styles_project.scss`: - URL patterns for target sites must **not** include `https://` protocol prefix - Advanced website indexing (upgrade from Basic) is required for AI summarization — takes 4-8 hours +### Rate Limiting +- Cloud Functions are stateless — in-memory rate limit counters don't work across scaled instances +- Firestore transactions provide atomic counter increments safe for concurrent access +- The rate limiter fails open — if Firestore is unavailable, searches proceed rather than being blocked +- Firestore TTL policies auto-clean expired rate limit documents — set on the `updatedAt` field + +--- + +## Part 9 — Deployment Status + +| Environment | URL | Status | +|---|---|---| +| Local dev | `http://localhost:1313` | ✅ Working | +| Staging | `https://stumbo.github.io/InterlispDraft.github.io/` | ✅ Deployed and verified | +| Production | `https://interlisp.org` | ⏳ Pending | + +### What is working end to end + +- Search box renders in Docsy navbar and sidebar +- Typing a query and pressing Enter redirects to `/search/?q=query` +- Vertex AI Search returns relevant results from the indexed interlisp.org site +- AI-generated summary appears at the top of results with `[n]` citation markers +- Citation sources are listed below the summary with links to source pages +- Snippets are plain text (HTML stripped) +- CORS is correctly scoped to allowed origins +- Rate limiting is active — 20 requests/minute per IP, 500/minute global +- Firestore TTL auto-cleans expired rate limit documents + --- -## Part 8 — Pending Items +## Part 10 — Remaining Items -- [ ] Confirm Advanced indexing completes (`indexingStatus: SUCCEEDED`) -- [ ] Verify AI summary appears in search results once indexing is complete -- [ ] Deploy to staging site (`stumbo.github.io/InterlispDraft.github.io`) -- [ ] Test CORS from staging domain - [ ] Deploy to production (`interlisp.org`) -- [ ] Set up CI/CD to re-index when site content changes (JSONL approach) -- [ ] Remove debug `console.log` statements from `index.js` -- [ ] Consider rate limiting on the Cloud Function for production +- [ ] Set up CI/CD to re-index when site content changes (JSONL approach or scheduled recrawl) +- [ ] Remove any remaining debug `console.log` statements from `index.js` +- [ ] Monitor Firestore `rate_limits` collection and adjust limits based on real traffic +- [ ] Consider adding query logging to Firestore for search analytics +- [ ] Review and tune the AI preamble prompt based on real query patterns +- [ ] Add `interlisp.org` to the CORS `allowedOrigins` list in `index.js` before production deploy (already present, verify it matches exact production domain) From b6f60f4507e4a1f3c73e60f8812b04db22426ede Mon Sep 17 00:00:00 2001 From: Bill Stumbo Date: Wed, 13 May 2026 08:05:28 -0400 Subject: [PATCH 3/4] Updates to search, clean up presentation of results --- assets/js/vertex-search.js | 52 +- assets/scss/_styles_project.scss | 46 +- layouts/search.html | 2 +- package-lock.json | 2462 +++++++++--------------------- package.json | 4 + search-function/index.js | 10 +- 6 files changed, 860 insertions(+), 1716 deletions(-) diff --git a/assets/js/vertex-search.js b/assets/js/vertex-search.js index 3bf4944c..e2ad69ae 100644 --- a/assets/js/vertex-search.js +++ b/assets/js/vertex-search.js @@ -1,6 +1,14 @@ +import { marked } from 'marked'; +import DOMPurify from 'dompurify'; + (function () { 'use strict'; + marked.setOptions({ + gfm: true, + breaks: true + }); + // Only run on the search results page const config = document.getElementById('vertex-search-config'); if (!config) return; @@ -29,6 +37,23 @@ .replace(/"/g, '"'); } + function renderSummaryMarkdown(summaryText, citationCount) { + const linkedMarkdown = (summaryText || '').replace(/\[(\d+)\]/g, (_, num) => { + const index = Number(num); + if (!Number.isInteger(index) || index < 1 || index > citationCount) return `[${num}]`; + return `[${num}](#source-row-${index})`; + }); + + const html = marked.parse(linkedMarkdown); + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: [ + 'a', 'p', 'ul', 'ol', 'li', 'strong', 'em', 'code', 'pre', + 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br' + ], + ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'id', 'class'] + }); + } + async function doSearch(q) { if (!q) { statusEl.textContent = 'Enter a search query above.'; @@ -62,16 +87,26 @@ // Show AI summary if available if (data.summary?.summaryText) { - summaryTxt.textContent = data.summary.summaryText; - summaryEl.style.display = 'block'; - - // Render numbered citation list const refs = data.summary?.citations || []; const validRefs = refs.filter(r => r.title || r.uri); - + + summaryTxt.innerHTML = renderSummaryMarkdown(data.summary.summaryText, validRefs.length); + + const inlineRefs = summaryTxt.querySelectorAll('a[href^="#source-row-"]'); + inlineRefs.forEach((el) => { + el.classList.add('summary-inline-citation'); + el.setAttribute('aria-label', `Jump to source ${el.textContent}`); + }); + + summaryEl.style.display = 'block'; + + // Remove stale source rows before rendering the latest list. + const previousCitations = summaryEl.querySelector('.search-citations'); + if (previousCitations) previousCitations.remove(); + if (validRefs.length > 0) { const citationHtml = validRefs.map((ref, i) => ` -
+
[${i + 1}] ${ref.uri ? `${escapeHtml(ref.title || ref.uri)}` @@ -79,12 +114,15 @@ }
`).join(''); - + const citationsDiv = document.createElement('div'); citationsDiv.className = 'search-citations mt-3'; citationsDiv.innerHTML = '

Sources

' + citationHtml; summaryEl.querySelector('.ai-summary').appendChild(citationsDiv); } + } else { + summaryTxt.innerHTML = ''; + summaryEl.style.display = 'none'; } // Show results diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 2e23b2fd..cce782c7 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -112,7 +112,51 @@ margin-bottom: 0.5rem; } - p { margin: 0; } + p { margin: 0 0 0.75rem; } + + p:last-child { + margin-bottom: 0; + } + + ul, + ol { + margin: 0.5rem 0 0.75rem 1.25rem; + } + + li { + margin-bottom: 0.35rem; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0.9rem 0 0.5rem; + font-size: 1rem; + font-weight: 700; + color: #124fa8; + } + + code { + color: #0b4aa2; + background: rgba(26, 115, 232, 0.1); + border-radius: 3px; + padding: 0 0.2rem; + } +} + +.summary-inline-citation { + margin-left: 0.1rem; + text-decoration: none; + font-weight: 600; + color: #1a73e8; + + &:hover, + &:focus { + text-decoration: underline; + } } .td-search-hit { diff --git a/layouts/search.html b/layouts/search.html index 4d917ab1..8739b3d1 100644 --- a/layouts/search.html +++ b/layouts/search.html @@ -17,7 +17,7 @@

Search Results

AI Summary -

+
diff --git a/package-lock.json b/package-lock.json index 82470702..3c49465a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,7 +2,9 @@ "name": "Interlisp.github.io", - "lockfileVersion": 2, + "version": "1.0.0", + + "lockfileVersion": 3, "requires": true, @@ -10,12 +12,14 @@ "": { - "name": "Interlisp.github.io", - "dependencies": { + "dompurify": "^3.3.1", + "jquery": "^3.7.1", + "marked": "^15.0.12", + "tabpanel": "^0.2.0" }, @@ -44,6 +48,8 @@ "dev": true, + "license": "ISC", + "dependencies": { "minipass": "^7.0.4" @@ -68,6 +74,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -94,6 +102,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">= 8" @@ -112,6 +122,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -128,67 +140,35 @@ }, - "node_modules/adm-zip": { - - "version": "0.5.16", - - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - - "dev": true, - - "engines": { - - "node": ">=12.0" - - } - - }, - - "node_modules/ansi-regex": { - - "version": "5.0.1", - - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "node_modules/@types/trusted-types": { - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "2.0.7", - "dev": true, + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "engines": { + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "node": ">=8" + "license": "MIT", - } + "optional": true }, - "node_modules/ansi-styles": { + "node_modules/adm-zip": { - "version": "4.3.0", + "version": "0.5.17", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", "dev": true, - "dependencies": { - - "color-convert": "^2.0.1" - - }, + "license": "MIT", "engines": { - "node": ">=8" - - }, - - "funding": { - - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=12.0" } @@ -196,14 +176,16 @@ "node_modules/anymatch": { - "version": "3.1.2", + "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", + "dependencies": { "normalize-path": "^3.0.0", @@ -222,11 +204,11 @@ "node_modules/autoprefixer": { - "version": "10.4.8", + "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", "dev": true, @@ -246,21 +228,29 @@ "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + + { + + "type": "github", + + "url": "https://github.com/sponsors/ai" + } ], - "dependencies": { + "license": "MIT", - "browserslist": "^4.21.3", + "dependencies": { - "caniuse-lite": "^1.0.30001373", + "browserslist": "^4.28.2", - "fraction.js": "^4.2.0", + "caniuse-lite": "^1.0.30001787", - "normalize-range": "^0.1.2", + "fraction.js": "^5.3.4", - "picocolors": "^1.0.0", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -286,20 +276,54 @@ }, + "node_modules/baseline-browser-mapping": { + + "version": "2.10.29", + + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + + "dev": true, + + "license": "Apache-2.0", + + "bin": { + + "baseline-browser-mapping": "dist/cli.cjs" + + }, + + "engines": { + + "node": ">=6.0.0" + + } + + }, + "node_modules/binary-extensions": { - "version": "2.2.0", + "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", + "engines": { "node": ">=8" + }, + + "funding": { + + "url": "https://github.com/sponsors/sindresorhus" + } }, @@ -314,6 +338,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "fill-range": "^7.1.1" @@ -330,11 +356,11 @@ "node_modules/browserslist": { - "version": "4.21.3", + "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, @@ -354,19 +380,31 @@ "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + + { + + "type": "github", + + "url": "https://github.com/sponsors/ai" + } ], + "license": "MIT", + "dependencies": { - "caniuse-lite": "^1.0.30001370", + "baseline-browser-mapping": "^2.10.12", - "electron-to-chromium": "^1.4.202", + "caniuse-lite": "^1.0.30001782", - "node-releases": "^2.0.6", + "electron-to-chromium": "^1.5.328", - "update-browserslist-db": "^1.0.5" + "node-releases": "^2.0.36", + + "update-browserslist-db": "^1.2.3" }, @@ -386,11 +424,11 @@ "node_modules/caniuse-lite": { - "version": "1.0.30001378", + "version": "1.0.30001792", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001378.tgz", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", - "integrity": "sha512-JVQnfoO7FK7WvU4ZkBRbPjaot4+YqxogSDosHv0Hv5mWpUESmN+UubMU6L/hGz8QlQ2aY5U0vR6MOs6j/CXpNA==", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", "dev": true, @@ -410,33 +448,33 @@ "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } + }, - ] + { - }, + "type": "github", - "node_modules/chokidar": { + "url": "https://github.com/sponsors/ai" - "version": "3.5.3", + } - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + ], - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "license": "CC-BY-4.0" - "dev": true, + }, - "funding": [ + "node_modules/chokidar": { - { + "version": "3.6.0", - "type": "individual", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "url": "https://paulmillr.com/funding/" + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - } + "dev": true, - ], + "license": "MIT", "dependencies": { @@ -462,6 +500,12 @@ }, + "funding": { + + "url": "https://paulmillr.com/funding/" + + }, + "optionalDependencies": { "fsevents": "~2.3.2" @@ -480,6 +524,8 @@ "dev": true, + "license": "BlueOak-1.0.0", + "engines": { "node": ">=18" @@ -490,93 +536,69 @@ "node_modules/cliui": { - "version": "7.0.4", + "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", + "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" - } - - }, - - "node_modules/color-convert": { - - "version": "2.0.1", - - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - - "dev": true, - - "dependencies": { - - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=12" } }, - "node_modules/color-name": { - - "version": "1.1.4", - - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - - "dev": true - - }, - - "node_modules/dependency-graph": { + "node_modules/cliui/node_modules/ansi-regex": { - "version": "0.11.0", + "version": "5.0.1", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", + "engines": { - "node": ">= 0.6.0" + "node": ">=8" } }, - "node_modules/dir-glob": { + "node_modules/cliui/node_modules/ansi-styles": { - "version": "3.0.1", + "version": "4.3.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", + "dependencies": { - "path-type": "^4.0.0" + "color-convert": "^2.0.1" }, @@ -584,23 +606,17 @@ "node": ">=8" - } - - }, - - "node_modules/electron-to-chromium": { - - "version": "1.4.224", + }, - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.224.tgz", + "funding": { - "integrity": "sha512-dOujC5Yzj0nOVE23iD5HKqrRSDj2SD7RazpZS/b/WX85MtO6/LzKDF4TlYZTBteB+7fvSg5JpWh0sN7fImNF8w==", + "url": "https://github.com/chalk/ansi-styles?sponsor=1" - "dev": true + } }, - "node_modules/emoji-regex": { + "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", @@ -608,91 +624,311 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + + "license": "MIT" }, - "node_modules/escalade": { + "node_modules/cliui/node_modules/string-width": { - "version": "3.1.1", + "version": "4.2.3", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", + + "dependencies": { + + "emoji-regex": "^8.0.0", + + "is-fullwidth-code-point": "^3.0.0", + + "strip-ansi": "^6.0.1" + + }, + "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/fast-glob": { + "node_modules/cliui/node_modules/strip-ansi": { - "version": "3.2.11", + "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "dependencies": { - - "@nodelib/fs.stat": "^2.0.2", - - "@nodelib/fs.walk": "^1.2.3", - - "glob-parent": "^5.1.2", + "license": "MIT", - "merge2": "^1.3.0", + "dependencies": { - "micromatch": "^4.0.4" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=8.6.0" + "node": ">=8" } }, - "node_modules/fastq": { + "node_modules/cliui/node_modules/wrap-ansi": { - "version": "1.13.0", + "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", + "dependencies": { - "reusify": "^1.0.4" + "ansi-styles": "^4.0.0", + + "string-width": "^4.1.0", + + "strip-ansi": "^6.0.0" + + }, + + "engines": { + + "node": ">=10" + + }, + + "funding": { + + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/fill-range": { + "node_modules/color-convert": { - "version": "7.1.1", + "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", + "dependencies": { - "to-regex-range": "^5.0.1" + "color-name": "~1.1.4" + + }, + + "engines": { + + "node": ">=7.0.0" + + } + + }, + + "node_modules/color-name": { + + "version": "1.1.4", + + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + + "dev": true, + + "license": "MIT" + + }, + + "node_modules/dependency-graph": { + + "version": "0.11.0", + + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + + "dev": true, + + "license": "MIT", + + "engines": { + + "node": ">= 0.6.0" + + } + + }, + + "node_modules/dir-glob": { + + "version": "3.0.1", + + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + + "dev": true, + + "license": "MIT", + + "dependencies": { + + "path-type": "^4.0.0" + + }, + + "engines": { + + "node": ">=8" + + } + + }, + + "node_modules/dompurify": { + + "version": "3.4.3", + + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.3.tgz", + + "integrity": "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==", + + "license": "(MPL-2.0 OR Apache-2.0)", + + "optionalDependencies": { + + "@types/trusted-types": "^2.0.7" + + } + + }, + + "node_modules/electron-to-chromium": { + + "version": "1.5.354", + + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz", + + "integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==", + + "dev": true, + + "license": "ISC" + + }, + + "node_modules/escalade": { + + "version": "3.2.0", + + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + + "dev": true, + + "license": "MIT", + + "engines": { + + "node": ">=6" + + } + + }, + + "node_modules/fast-glob": { + + "version": "3.3.3", + + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + + "dev": true, + + "license": "MIT", + + "dependencies": { + + "@nodelib/fs.stat": "^2.0.2", + + "@nodelib/fs.walk": "^1.2.3", + + "glob-parent": "^5.1.2", + + "merge2": "^1.3.0", + + "micromatch": "^4.0.8" + + }, + + "engines": { + + "node": ">=8.6.0" + + } + + }, + + "node_modules/fastq": { + + "version": "1.20.1", + + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + + "dev": true, + + "license": "ISC", + + "dependencies": { + + "reusify": "^1.0.4" + + } + + }, + + "node_modules/fill-range": { + + "version": "7.1.1", + + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + + "dev": true, + + "license": "MIT", + + "dependencies": { + + "to-regex-range": "^5.0.1" }, @@ -706,14 +942,16 @@ "node_modules/fraction.js": { - "version": "4.2.0", + "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, + "license": "MIT", + "engines": { "node": "*" @@ -722,9 +960,9 @@ "funding": { - "type": "patreon", + "type": "github", - "url": "https://www.patreon.com/infusion" + "url": "https://github.com/sponsors/rawify" } @@ -732,14 +970,16 @@ "node_modules/fs-extra": { - "version": "10.1.0", + "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, + "license": "MIT", + "dependencies": { "graceful-fs": "^4.2.0", @@ -752,7 +992,7 @@ "engines": { - "node": ">=12" + "node": ">=14.14" } @@ -760,16 +1000,18 @@ "node_modules/fsevents": { - "version": "2.3.2", + "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", + "optional": true, "os": [ @@ -796,6 +1038,8 @@ "dev": true, + "license": "ISC", + "engines": { "node": "6.* || 8.* || >= 10.*" @@ -814,6 +1058,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">=12" @@ -838,6 +1084,8 @@ "dev": true, + "license": "ISC", + "dependencies": { "is-glob": "^4.0.1" @@ -854,21 +1102,23 @@ "node_modules/globby": { - "version": "13.1.2", + "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", "dev": true, + "license": "MIT", + "dependencies": { "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.0", - "ignore": "^5.2.0", + "ignore": "^5.2.4", "merge2": "^1.4.1", @@ -890,15 +1140,43 @@ }, + "node_modules/globby/node_modules/slash": { + + "version": "4.0.0", + + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + + "dev": true, + + "license": "MIT", + + "engines": { + + "node": ">=12" + + }, + + "funding": { + + "url": "https://github.com/sponsors/sindresorhus" + + } + + }, + "node_modules/graceful-fs": { - "version": "4.2.10", + "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + + "license": "ISC" }, @@ -914,6 +1192,8 @@ "hasInstallScript": true, + "license": "MIT", + "dependencies": { "adm-zip": "^0.5.16", @@ -940,14 +1220,16 @@ "node_modules/ignore": { - "version": "5.2.0", + "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", + "engines": { "node": ">= 4" @@ -966,6 +1248,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "binary-extensions": "^2.0.0" @@ -990,6 +1274,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">=0.10.0" @@ -1008,6 +1294,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">=8" @@ -1026,6 +1314,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "is-extglob": "^2.1.1" @@ -1050,6 +1340,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">=0.12.0" @@ -1064,20 +1356,24 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + + "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.1.0", + "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, + "license": "MIT", + "dependencies": { "universalify": "^2.0.0" @@ -1094,17 +1390,49 @@ "node_modules/lilconfig": { - "version": "2.0.6", + "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, + "license": "MIT", + "engines": { - "node": ">=10" + "node": ">=14" + + }, + + "funding": { + + "url": "https://github.com/sponsors/antonk52" + + } + + }, + + "node_modules/marked": { + + "version": "15.0.12", + + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + + "license": "MIT", + + "bin": { + + "marked": "bin/marked.js" + + }, + + "engines": { + + "node": ">= 18" } @@ -1120,6 +1448,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">= 8" @@ -1138,6 +1468,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "braces": "^3.0.3", @@ -1156,14 +1488,16 @@ "node_modules/minipass": { - "version": "7.1.2", + "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, + "license": "BlueOak-1.0.0", + "engines": { "node": ">=16 || 14 >=14.17" @@ -1182,6 +1516,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "minipass": "^7.1.2" @@ -1198,11 +1534,11 @@ "node_modules/nanoid": { - "version": "3.3.11", + "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, @@ -1218,6 +1554,8 @@ ], + "license": "MIT", + "bin": { "nanoid": "bin/nanoid.cjs" @@ -1234,13 +1572,15 @@ "node_modules/node-releases": { - "version": "2.0.6", + "version": "2.0.44", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + + "dev": true, - "dev": true + "license": "MIT" }, @@ -1254,6 +1594,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">=0.10.0" @@ -1262,64 +1604,52 @@ }, - "node_modules/normalize-range": { + "node_modules/path-type": { - "version": "0.1.2", + "version": "4.0.0", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", + "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/path-type": { + "node_modules/picocolors": { - "version": "4.0.0", - - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - - "dev": true, - - "engines": { - - "node": ">=8" - - } - - }, - - "node_modules/picocolors": { - - "version": "1.1.1", + "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", + "engines": { "node": ">=8.6" @@ -1344,6 +1674,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">=0.10.0" @@ -1354,11 +1686,11 @@ "node_modules/postcss": { - "version": "8.5.6", + "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, @@ -1390,6 +1722,8 @@ ], + "license": "MIT", + "dependencies": { "nanoid": "^3.3.11", @@ -1410,21 +1744,23 @@ "node_modules/postcss-cli": { - "version": "10.0.0", + "version": "10.1.0", - "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.0.0.tgz", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.1.0.tgz", - "integrity": "sha512-Wjy/00wBBEgQqnSToznxLWDnATznokFGXsHtF/3G8glRZpz5KYlfHcBW/VMJmWAeF2x49zjgy4izjM3/Wx1dKA==", + "integrity": "sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==", "dev": true, + "license": "MIT", + "dependencies": { "chokidar": "^3.3.0", "dependency-graph": "^0.11.0", - "fs-extra": "^10.0.0", + "fs-extra": "^11.0.0", "get-stdin": "^9.0.0", @@ -1440,7 +1776,7 @@ "read-cache": "^1.0.0", - "slash": "^4.0.0", + "slash": "^5.0.0", "yargs": "^17.0.0" @@ -1468,33 +1804,47 @@ "node_modules/postcss-load-config": { - "version": "4.0.1", + "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "dev": true, - "dependencies": { + "funding": [ - "lilconfig": "^2.0.5", + { - "yaml": "^2.1.1" + "type": "opencollective", - }, + "url": "https://opencollective.com/postcss/" - "engines": { + }, - "node": ">= 14" + { - }, + "type": "github", - "funding": { + "url": "https://github.com/sponsors/ai" + + } + + ], + + "license": "MIT", + + "dependencies": { - "type": "opencollective", + "lilconfig": "^3.0.0", - "url": "https://opencollective.com/postcss/" + "yaml": "^2.3.4" + + }, + + "engines": { + + "node": ">= 14" }, @@ -1526,14 +1876,36 @@ "node_modules/postcss-reporter": { - "version": "7.0.5", + "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.5.tgz", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.1.0.tgz", - "integrity": "sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==", + "integrity": "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==", "dev": true, + "funding": [ + + { + + "type": "opencollective", + + "url": "https://opencollective.com/postcss/" + + }, + + { + + "type": "github", + + "url": "https://github.com/sponsors/ai" + + } + + ], + + "license": "MIT", + "dependencies": { "picocolors": "^1.0.0", @@ -1548,14 +1920,6 @@ }, - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/postcss/" - - }, - "peerDependencies": { "postcss": "^8.1.0" @@ -1572,7 +1936,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + + "license": "MIT" }, @@ -1586,6 +1952,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">= 0.8" @@ -1630,7 +1998,9 @@ } - ] + ], + + "license": "MIT" }, @@ -1644,6 +2014,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "pify": "^2.3.0" @@ -1662,6 +2034,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "picomatch": "^2.2.1" @@ -1686,6 +2060,8 @@ "dev": true, + "license": "MIT", + "engines": { "node": ">=0.10.0" @@ -1696,14 +2072,16 @@ "node_modules/reusify": { - "version": "1.0.4", + "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", + "engines": { "iojs": ">=1.0.0", @@ -1752,6 +2130,8 @@ ], + "license": "MIT", + "dependencies": { "queue-microtask": "^1.2.2" @@ -1762,17 +2142,19 @@ "node_modules/slash": { - "version": "4.0.0", + "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", + "engines": { - "node": ">=12" + "node": ">=14.16" }, @@ -1794,61 +2176,11 @@ "dev": true, - "engines": { - - "node": ">=0.10.0" - - } - - }, - - "node_modules/string-width": { - - "version": "4.2.3", - - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - - "dev": true, - - "dependencies": { - - "emoji-regex": "^8.0.0", - - "is-fullwidth-code-point": "^3.0.0", - - "strip-ansi": "^6.0.1" - - }, - - "engines": { - - "node": ">=8" - - } - - }, - - "node_modules/strip-ansi": { - - "version": "6.0.1", - - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - - "dev": true, - - "dependencies": { - - "ansi-regex": "^5.0.1" - - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=8" + "node": ">=0.10.0" } @@ -1866,14 +2198,16 @@ "node_modules/tar": { - "version": "7.5.7", + "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -1898,13 +2232,15 @@ "node_modules/thenby": { - "version": "1.3.4", + "version": "1.4.1", + + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.4.1.tgz", - "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", + "integrity": "sha512-D5a/bO0KdalOE3q8MlrRmSxjbKZHT3MQmXkJP+r97Vw8MMwOZKOwUSEyTtK7eSMj2y0kyAjpYMRMZmmLw1FtNQ==", - "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "dev": true, - "dev": true + "license": "Apache-2.0" }, @@ -1918,6 +2254,8 @@ "dev": true, + "license": "MIT", + "dependencies": { "is-number": "^7.0.0" @@ -1934,14 +2272,16 @@ "node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", + "engines": { "node": ">= 10.0.0" @@ -1952,11 +2292,11 @@ "node_modules/update-browserslist-db": { - "version": "1.0.5", + "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, @@ -1976,61 +2316,37 @@ "url": "https://tidelift.com/funding/github/npm/browserslist" - } - - ], - - "dependencies": { - - "escalade": "^3.1.1", - - "picocolors": "^1.0.0" - - }, - - "bin": { - - "browserslist-lint": "cli.js" - - }, - - "peerDependencies": { - - "browserslist": ">= 4.21.0" - - } + }, - }, + { - "node_modules/wrap-ansi": { + "type": "github", - "version": "7.0.0", + "url": "https://github.com/sponsors/ai" - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + } - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + ], - "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - - "string-width": "^4.1.0", + "escalade": "^3.2.0", - "strip-ansi": "^6.0.0" + "picocolors": "^1.1.1" }, - "engines": { + "bin": { - "node": ">=10" + "update-browserslist-db": "cli.js" }, - "funding": { + "peerDependencies": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "browserslist": ">= 4.21.0" } @@ -2046,6 +2362,8 @@ "dev": true, + "license": "ISC", + "engines": { "node": ">=10" @@ -2064,6 +2382,8 @@ "dev": true, + "license": "BlueOak-1.0.0", + "engines": { "node": ">=18" @@ -2074,14 +2394,16 @@ "node_modules/yaml": { - "version": "2.8.2", + "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, + "license": "ISC", + "bin": { "yaml": "bin.mjs" @@ -2104,17 +2426,19 @@ "node_modules/yargs": { - "version": "17.5.1", + "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", + "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2126,7 +2450,7 @@ "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, @@ -2148,1376 +2472,104 @@ "dev": true, + "license": "ISC", + "engines": { "node": ">=12" } - } - - }, - - "dependencies": { + }, - "@isaacs/fs-minipass": { + "node_modules/yargs/node_modules/ansi-regex": { - "version": "4.0.1", + "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "requires": { + "license": "MIT", - "minipass": "^7.0.4" + "engines": { + + "node": ">=8" } }, - "@nodelib/fs.scandir": { + "node_modules/yargs/node_modules/emoji-regex": { - "version": "2.1.5", + "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "requires": { - - "@nodelib/fs.stat": "2.0.5", - - "run-parallel": "^1.1.9" - - } + "license": "MIT" }, - "@nodelib/fs.stat": { - - "version": "2.0.5", + "node_modules/yargs/node_modules/string-width": { - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "version": "4.2.3", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "dev": true + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - }, + "dev": true, - "@nodelib/fs.walk": { + "license": "MIT", - "version": "1.2.8", + "dependencies": { - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "emoji-regex": "^8.0.0", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "is-fullwidth-code-point": "^3.0.0", - "dev": true, + "strip-ansi": "^6.0.1" - "requires": { + }, - "@nodelib/fs.scandir": "2.1.5", + "engines": { - "fastq": "^1.6.0" + "node": ">=8" } }, - "adm-zip": { + "node_modules/yargs/node_modules/strip-ansi": { - "version": "0.5.16", + "version": "6.0.1", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true + "dev": true, - }, + "license": "MIT", - "ansi-regex": { - - "version": "5.0.1", - - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - - "dev": true - - }, - - "ansi-styles": { - - "version": "4.3.0", - - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - - "dev": true, - - "requires": { - - "color-convert": "^2.0.1" - - } - - }, - - "anymatch": { - - "version": "3.1.2", - - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - - "dev": true, - - "requires": { - - "normalize-path": "^3.0.0", - - "picomatch": "^2.0.4" - - } - - }, - - "autoprefixer": { - - "version": "10.4.8", - - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz", - - "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", - - "dev": true, - - "requires": { - - "browserslist": "^4.21.3", - - "caniuse-lite": "^1.0.30001373", - - "fraction.js": "^4.2.0", - - "normalize-range": "^0.1.2", - - "picocolors": "^1.0.0", - - "postcss-value-parser": "^4.2.0" - - } - - }, - - "binary-extensions": { - - "version": "2.2.0", - - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - - "dev": true - - }, - - "braces": { - - "version": "3.0.3", - - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - - "dev": true, - - "requires": { - - "fill-range": "^7.1.1" - - } - - }, - - "browserslist": { - - "version": "4.21.3", - - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", - - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", - - "dev": true, - - "requires": { - - "caniuse-lite": "^1.0.30001370", - - "electron-to-chromium": "^1.4.202", - - "node-releases": "^2.0.6", - - "update-browserslist-db": "^1.0.5" - - } - - }, - - "caniuse-lite": { - - "version": "1.0.30001378", - - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001378.tgz", - - "integrity": "sha512-JVQnfoO7FK7WvU4ZkBRbPjaot4+YqxogSDosHv0Hv5mWpUESmN+UubMU6L/hGz8QlQ2aY5U0vR6MOs6j/CXpNA==", - - "dev": true - - }, - - "chokidar": { - - "version": "3.5.3", - - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - - "dev": true, - - "requires": { - - "anymatch": "~3.1.2", - - "braces": "~3.0.2", - - "fsevents": "~2.3.2", - - "glob-parent": "~5.1.2", - - "is-binary-path": "~2.1.0", - - "is-glob": "~4.0.1", - - "normalize-path": "~3.0.0", - - "readdirp": "~3.6.0" - - } - - }, - - "chownr": { - - "version": "3.0.0", - - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - - "dev": true - - }, - - "cliui": { - - "version": "7.0.4", - - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - - "dev": true, - - "requires": { - - "string-width": "^4.2.0", - - "strip-ansi": "^6.0.0", - - "wrap-ansi": "^7.0.0" - - } - - }, - - "color-convert": { - - "version": "2.0.1", - - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - - "dev": true, - - "requires": { - - "color-name": "~1.1.4" - - } - - }, - - "color-name": { - - "version": "1.1.4", - - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - - "dev": true - - }, - - "dependency-graph": { - - "version": "0.11.0", - - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - - "dev": true - - }, - - "dir-glob": { - - "version": "3.0.1", - - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - - "dev": true, - - "requires": { - - "path-type": "^4.0.0" - - } - - }, - - "electron-to-chromium": { - - "version": "1.4.224", - - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.224.tgz", - - "integrity": "sha512-dOujC5Yzj0nOVE23iD5HKqrRSDj2SD7RazpZS/b/WX85MtO6/LzKDF4TlYZTBteB+7fvSg5JpWh0sN7fImNF8w==", - - "dev": true - - }, - - "emoji-regex": { - - "version": "8.0.0", - - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - - "dev": true - - }, - - "escalade": { - - "version": "3.1.1", - - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - - "dev": true - - }, - - "fast-glob": { - - "version": "3.2.11", - - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - - "dev": true, - - "requires": { - - "@nodelib/fs.stat": "^2.0.2", - - "@nodelib/fs.walk": "^1.2.3", - - "glob-parent": "^5.1.2", - - "merge2": "^1.3.0", - - "micromatch": "^4.0.4" - - } - - }, - - "fastq": { - - "version": "1.13.0", - - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - - "dev": true, - - "requires": { - - "reusify": "^1.0.4" - - } - - }, - - "fill-range": { - - "version": "7.1.1", - - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - - "dev": true, - - "requires": { - - "to-regex-range": "^5.0.1" - - } - - }, - - "fraction.js": { - - "version": "4.2.0", - - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - - "dev": true - - }, - - "fs-extra": { - - "version": "10.1.0", - - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - - "dev": true, - - "requires": { - - "graceful-fs": "^4.2.0", - - "jsonfile": "^6.0.1", - - "universalify": "^2.0.0" - - } - - }, - - "fsevents": { - - "version": "2.3.2", - - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - - "dev": true, - - "optional": true - - }, - - "get-caller-file": { - - "version": "2.0.5", - - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - - "dev": true - - }, - - "get-stdin": { - - "version": "9.0.0", - - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", - - "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", - - "dev": true - - }, - - "glob-parent": { - - "version": "5.1.2", - - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - - "dev": true, - - "requires": { - - "is-glob": "^4.0.1" - - } - - }, - - "globby": { - - "version": "13.1.2", - - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", - - "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", - - "dev": true, - - "requires": { - - "dir-glob": "^3.0.1", - - "fast-glob": "^3.2.11", - - "ignore": "^5.2.0", - - "merge2": "^1.4.1", - - "slash": "^4.0.0" - - } - - }, - - "graceful-fs": { - - "version": "4.2.10", - - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - - "dev": true - - }, - - "hugo-extended": { - - "version": "0.155.3", - - "resolved": "https://registry.npmjs.org/hugo-extended/-/hugo-extended-0.155.3.tgz", - - "integrity": "sha512-nzGmsgnOdeOGDgtpPHEPZ1PVizDHPU3240UdRmxu0b9vT+A7iB9toaUGcUQXQ/PVURq6y8lIuoTFsI4xfDoLLA==", - - "dev": true, - - "requires": { - - "adm-zip": "^0.5.16", - - "tar": "^7.5.7" - - } - - }, - - "ignore": { - - "version": "5.2.0", - - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - - "dev": true - - }, - - "is-binary-path": { - - "version": "2.1.0", - - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - - "dev": true, - - "requires": { - - "binary-extensions": "^2.0.0" - - } - - }, - - "is-extglob": { - - "version": "2.1.1", - - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - - "dev": true - - }, - - "is-fullwidth-code-point": { - - "version": "3.0.0", - - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - - "dev": true - - }, - - "is-glob": { - - "version": "4.0.3", - - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - - "dev": true, - - "requires": { - - "is-extglob": "^2.1.1" - - } - - }, - - "is-number": { - - "version": "7.0.0", - - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - - "dev": true - - }, - - "jquery": { - - "version": "3.7.1", - - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - - }, - - "jsonfile": { - - "version": "6.1.0", - - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - - "dev": true, - - "requires": { - - "graceful-fs": "^4.1.6", - - "universalify": "^2.0.0" - - } - - }, - - "lilconfig": { - - "version": "2.0.6", - - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", - - "dev": true - - }, - - "merge2": { - - "version": "1.4.1", - - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - - "dev": true - - }, - - "micromatch": { - - "version": "4.0.8", - - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - - "dev": true, - - "requires": { - - "braces": "^3.0.3", - - "picomatch": "^2.3.1" - - } - - }, - - "minipass": { - - "version": "7.1.2", - - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - - "dev": true - - }, - - "minizlib": { - - "version": "3.1.0", - - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - - "dev": true, - - "requires": { - - "minipass": "^7.1.2" - - } - - }, - - "nanoid": { - - "version": "3.3.11", - - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - - "dev": true - - }, - - "node-releases": { - - "version": "2.0.6", - - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - - "dev": true - - }, - - "normalize-path": { - - "version": "3.0.0", - - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - - "dev": true - - }, - - "normalize-range": { - - "version": "0.1.2", - - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - - "dev": true - - }, - - "path-type": { - - "version": "4.0.0", - - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - - "dev": true - - }, - - "picocolors": { - - "version": "1.1.1", - - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - - "dev": true - - }, - - "picomatch": { - - "version": "2.3.1", - - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - - "dev": true - - }, - - "pify": { - - "version": "2.3.0", - - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - - "dev": true - - }, - - "postcss": { - - "version": "8.5.6", - - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - - "dev": true, - - "requires": { - - "nanoid": "^3.3.11", - - "picocolors": "^1.1.1", - - "source-map-js": "^1.2.1" - - } - - }, - - "postcss-cli": { - - "version": "10.0.0", - - "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.0.0.tgz", - - "integrity": "sha512-Wjy/00wBBEgQqnSToznxLWDnATznokFGXsHtF/3G8glRZpz5KYlfHcBW/VMJmWAeF2x49zjgy4izjM3/Wx1dKA==", - - "dev": true, - - "requires": { - - "chokidar": "^3.3.0", - - "dependency-graph": "^0.11.0", - - "fs-extra": "^10.0.0", - - "get-stdin": "^9.0.0", - - "globby": "^13.0.0", - - "picocolors": "^1.0.0", - - "postcss-load-config": "^4.0.0", - - "postcss-reporter": "^7.0.0", - - "pretty-hrtime": "^1.0.3", - - "read-cache": "^1.0.0", - - "slash": "^4.0.0", - - "yargs": "^17.0.0" - - } - - }, - - "postcss-load-config": { - - "version": "4.0.1", - - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", - - "dev": true, - - "requires": { - - "lilconfig": "^2.0.5", - - "yaml": "^2.1.1" - - } - - }, - - "postcss-reporter": { - - "version": "7.0.5", - - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.5.tgz", - - "integrity": "sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==", - - "dev": true, - - "requires": { - - "picocolors": "^1.0.0", - - "thenby": "^1.3.4" - - } - - }, - - "postcss-value-parser": { - - "version": "4.2.0", - - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - - "dev": true - - }, - - "pretty-hrtime": { - - "version": "1.0.3", - - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", - - "dev": true - - }, - - "queue-microtask": { - - "version": "1.2.3", - - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - - "dev": true - - }, - - "read-cache": { - - "version": "1.0.0", - - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - - "dev": true, - - "requires": { - - "pify": "^2.3.0" - - } - - }, - - "readdirp": { - - "version": "3.6.0", - - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - - "dev": true, - - "requires": { - - "picomatch": "^2.2.1" - - } - - }, - - "require-directory": { - - "version": "2.1.1", - - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - - "dev": true - - }, - - "reusify": { - - "version": "1.0.4", - - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - - "dev": true - - }, - - "run-parallel": { - - "version": "1.2.0", - - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - - "dev": true, - - "requires": { - - "queue-microtask": "^1.2.2" - - } - - }, - - "slash": { - - "version": "4.0.0", - - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - - "dev": true - - }, - - "source-map-js": { - - "version": "1.2.1", - - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - - "dev": true - - }, - - "string-width": { - - "version": "4.2.3", - - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - - "dev": true, - - "requires": { - - "emoji-regex": "^8.0.0", - - "is-fullwidth-code-point": "^3.0.0", - - "strip-ansi": "^6.0.1" - - } - - }, - - "strip-ansi": { - - "version": "6.0.1", - - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - - "dev": true, - - "requires": { + "dependencies": { "ansi-regex": "^5.0.1" - } - - }, - - "tabpanel": { - - "version": "0.2.0", - - "resolved": "https://registry.npmjs.org/tabpanel/-/tabpanel-0.2.0.tgz", - - "integrity": "sha512-tS6UG1L/QfZZ0GdqMKkPXSYtErQNE0qDxNpo0xJh0XZT9TYjZ0lLHwIJUhhyV+u01h1iSswrw6vCNYqwD/R+EQ==" - - }, - - "tar": { - - "version": "7.5.7", - - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", - - "dev": true, - - "requires": { - - "@isaacs/fs-minipass": "^4.0.0", - - "chownr": "^3.0.0", - - "minipass": "^7.1.2", - - "minizlib": "^3.1.0", - - "yallist": "^5.0.0" - - } - - }, - - "thenby": { - - "version": "1.3.4", - - "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", - - "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", - - "dev": true - - }, - - "to-regex-range": { - - "version": "5.0.1", - - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - - "dev": true, - - "requires": { - - "is-number": "^7.0.0" - - } - - }, - - "universalify": { - - "version": "2.0.0", - - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - - "dev": true - - }, - - "update-browserslist-db": { - - "version": "1.0.5", - - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", - - "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", - - "dev": true, - - "requires": { - - "escalade": "^3.1.1", - - "picocolors": "^1.0.0" - - } - - }, - - "wrap-ansi": { - - "version": "7.0.0", - - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - - "dev": true, - - "requires": { - - "ansi-styles": "^4.0.0", - - "string-width": "^4.1.0", - - "strip-ansi": "^6.0.0" - - } - - }, - - "y18n": { - - "version": "5.0.8", - - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - - "dev": true - - }, - - "yallist": { - - "version": "5.0.0", - - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - - "dev": true - - }, - - "yaml": { - - "version": "2.8.2", - - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - - "dev": true - - }, - - "yargs": { - - "version": "17.5.1", - - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", - - "dev": true, - - "requires": { - - "cliui": "^7.0.2", - - "escalade": "^3.1.1", - - "get-caller-file": "^2.0.5", - - "require-directory": "^2.1.1", - - "string-width": "^4.2.3", + }, - "y18n": "^5.0.5", + "engines": { - "yargs-parser": "^21.0.0" + "node": ">=8" } - }, - - "yargs-parser": { - - "version": "21.1.1", - - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - - "dev": true - } } diff --git a/package.json b/package.json index 7acc342b..cd73ea02 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,12 @@ "dependencies": { + "dompurify": "^3.3.1", + "jquery": "^3.7.1", + "marked": "^15.0.12", + "tabpanel": "^0.2.0" } diff --git a/search-function/index.js b/search-function/index.js index 5d0c90ae..31133575 100644 --- a/search-function/index.js +++ b/search-function/index.js @@ -153,8 +153,14 @@ exports.search = async (req, res) => { function buildPreamble(context) { const base = `You are a search assistant for the Interlisp documentation site. -Answer questions clearly and concisely. Always cite the sources you used. -If no relevant results exist, say so directly rather than guessing.`; +Answer in strict Markdown only. +Use this structure exactly when applicable: +- One short opening paragraph. +- "## Key Points" followed by bullet points. +- "## Details" for additional context. +- "## Caveats" only when needed. +Always cite the sources you used using numeric markers like [1], [2], [3]. +Do not emit HTML. If no relevant results exist, say so directly rather than guessing.`; if (context) { return `${base}\nThe user is currently browsing the "${context}" section — prioritize results from that section where relevant.`; From 592734cd4fafee2edee4e3b5d3720133ed2b6546 Mon Sep 17 00:00:00 2001 From: Bill Stumbo Date: Mon, 18 May 2026 23:55:19 -0400 Subject: [PATCH 4/4] Add github contents to search, update search display to include them JS program to read contents of several Interlisp.org repos capturing information on markdown documents, PRs, issues and discussions. Contents are written as JSONL and can be imported into a storage bucket and imported into the search engine. Search results will include these elements. Update search results presentation to include github items, improve formatting of returned items. This aspect is still a work in progress. --- assets/js/vertex-search.js | 93 ++++- assets/scss/_styles_project.scss | 44 +- scripts/github-index.js | 672 +++++++++++++++++++++++++++++++ search-function/index.js | 36 +- 4 files changed, 829 insertions(+), 16 deletions(-) create mode 100644 scripts/github-index.js diff --git a/assets/js/vertex-search.js b/assets/js/vertex-search.js index e2ad69ae..179dd05c 100644 --- a/assets/js/vertex-search.js +++ b/assets/js/vertex-search.js @@ -54,6 +54,86 @@ import DOMPurify from 'dompurify'; }); } + function renderResultSnippet(snippetText, result) { + if (!snippetText) return ''; + const normalized = normalizeSnippetText(snippetText, result); + const html = marked.parse(normalized); + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'b', 'strong', 'em', 'code', 'pre', 'br', 'ul', 'ol', 'li', 'a'], + ALLOWED_ATTR: ['href', 'title', 'target', 'rel'] + }); + } + + function decodeHtmlEntities(text) { + const textarea = document.createElement('textarea'); + textarea.innerHTML = String(text || ''); + return textarea.value; + } + + function normalizeSnippetText(text, result) { + let output = decodeHtmlEntities(text); + + // Repair malformed highlight markers like "bIssue/b" or "bmedley issues/b". + output = output.replace(/(^|[\s(\[{])b([^\n]{1,120}?)\/b(?=[\s)\]}.,;:!?]|$)/gi, + (match, prefix, value) => `${prefix}${value.trim()}`); + + // Repair malformed URLs where emphasis markers leak into the URL text. + output = output + .replace(/https?:\/\/b/gi, 'https://') + .replace(/\bb([a-z0-9.-]+)\/b/gi, '$1') + .replace(/\/(b)([a-z0-9._-]+)/gi, '/$2') + .replace(/([a-z0-9._-]+)\/b(?=\/|\b)/gi, '$1') + .replace(/\s*\.\.\.\s*/g, ' '); + + output = normalizeGithubIssueMentions(output, result); + + // Drop noisy raw URL tails often appended by snippet extraction. + output = output.replace(/https?:\/\/\S+$/i, ''); + + // Normalize whitespace for cleaner one-line snippets. + output = output.replace(/\s+/g, ' ').trim(); + return output; + } + + function normalizeGithubIssueMentions(text, result) { + const repo = result?.repo; + if (!repo) return text; + + // Fix malformed markdown links like: [Issue 609](https://bgithub/b.com/Interlisp/bmedley/b/bissues/b/609) + let output = text.replace( + /\[(Issue\s+#?\d+)\]\((https?:\/\/[^)]+)\)/gi, + (match, label, rawUrl) => { + const issueNumberMatch = label.match(/(\d+)/); + if (!issueNumberMatch) return match; + const issueNumber = issueNumberMatch[1]; + const cleanUrl = `https://github.com/Interlisp/${repo}/issues/${issueNumber}`; + return `[${label}](${cleanUrl})`; + } + ); + + // Convert plain text mentions like "Issue 609" or "Issue #609" into links. + output = output.replace(/\bIssue\s+#?(\d+)\b/g, (match, num) => { + const issueUrl = `https://github.com/Interlisp/${repo}/issues/${num}`; + return `[Issue ${num}](${issueUrl})`; + }); + + return output; + } + + function formatDisplayUrl(rawUrl) { + if (!rawUrl) return ''; + + try { + const parsed = new URL(rawUrl); + const host = parsed.host.replace(/^www\./, ''); + const pathSegments = parsed.pathname.split('/').filter(Boolean); + const path = pathSegments.length > 0 ? ` › ${pathSegments.join(' › ')}` : ''; + return `${host}${path}`; + } catch (_) { + return rawUrl; + } + } + async function doSearch(q) { if (!q) { statusEl.textContent = 'Enter a search query above.'; @@ -136,11 +216,18 @@ import DOMPurify from 'dompurify'; `

${data.results.length} results for ${escapeHtml(q)}

` + data.results.map(r => `
-
+
${escapeHtml(r.title || 'Untitled')}
- ${r.snippet ? `

${escapeHtml(r.snippet)}

` : ''} -

${escapeHtml(r.url)}

+

${escapeHtml(formatDisplayUrl(r.url))}

+ ${r.snippet ? `
${renderResultSnippet(r.snippet, r)}
` : ''} + ${(r.type || r.repo || r.state) + ? `

+ ${r.type ? `${escapeHtml(r.type)}` : ''} + ${r.repo ? `${escapeHtml(r.repo)}` : ''} + ${r.state ? `${escapeHtml(r.state)}` : ''} +

` + : ''}
`).join(''); diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index cce782c7..d1c67314 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -168,19 +168,59 @@ &__title { margin: 0 0 0.25rem; font-size: 1rem; - a { color: #1a0dab; } + line-height: 1.35; + a { + color: #1a0dab; + text-decoration: none; + } + + a:hover, + a:focus { + text-decoration: underline; + } } &__snippet { font-size: 0.875rem; color: #545454; margin: 0.25rem 0; + + p { + margin: 0 0 0.4rem; + } + + p:last-child { + margin-bottom: 0; + } + + b, + strong { + color: #1f1f1f; + font-weight: 700; + } } &__url { - color: #006621; + color: #0b8043; margin: 0; + word-break: break-word; } + + &__meta { + margin-top: 0.4rem; + } +} + +.search-meta-chip { + display: inline-block; + margin-right: 0.35rem; + margin-bottom: 0.2rem; + padding: 0.08rem 0.45rem; + font-size: 0.72rem; + border-radius: 999px; + background: #eef2f7; + color: #4a5568; + border: 1px solid #dde3eb; } .td-search-loading, diff --git a/scripts/github-index.js b/scripts/github-index.js new file mode 100644 index 00000000..91bec410 --- /dev/null +++ b/scripts/github-index.js @@ -0,0 +1,672 @@ +const fs = require('fs'); +const path = require('path'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || process.env.GITHUB_PAT || process.env.GH_TOKEN || ''; +const ORG = 'Interlisp'; +const OUTPUT_FILE = path.join(__dirname, '../github-index.jsonl'); +const DEBUG = process.env.DEBUG_GITHUB_INDEX === '1'; +const START_TIME = Date.now(); + +if (!GITHUB_TOKEN) { + throw new Error('Missing GitHub token. Set GITHUB_TOKEN, GITHUB_PAT, or GH_TOKEN in the environment.'); +} + +const headers = { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' +}; + +const FETCH_TIMEOUT_MS = 30000; +const FETCH_RETRIES = 2; +const MAX_PAGES_PER_PAGINATION = Number.parseInt(process.env.GITHUB_INDEX_MAX_PAGES || '', 10); +const MAX_RATE_LIMIT_RETRIES = Number.parseInt(process.env.GITHUB_RATE_LIMIT_RETRIES || '6', 10); +const SECONDARY_LIMIT_BASE_WAIT_MS = Number.parseInt(process.env.GITHUB_SECONDARY_LIMIT_BASE_WAIT_MS || '60000', 10); +const SECONDARY_LIMIT_MAX_WAIT_MS = Number.parseInt(process.env.GITHUB_SECONDARY_LIMIT_MAX_WAIT_MS || '900000', 10); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + return await fetch(url, { ...options, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +async function githubRequest(url, options = {}, label = 'request') { + let lastError; + + for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt += 1) { + try { + const response = await fetchWithTimeout(url, options); + if (response.ok) { + return response; + } + + const status = response.status; + const retryAfter = response.headers.get('retry-after'); + const remainingRaw = response.headers.get('x-ratelimit-remaining'); + const resetRaw = response.headers.get('x-ratelimit-reset'); + const remaining = Number.parseInt(remainingRaw || '', 10); + const resetEpoch = Number.parseInt(resetRaw || '', 10); + + let waitMs = null; + let reason = ''; + + if (retryAfter) { + const retryAfterSec = Number.parseInt(retryAfter, 10); + if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) { + waitMs = retryAfterSec * 1000; + reason = `retry-after=${retryAfterSec}s`; + } + } + + if (waitMs === null && remaining === 0 && Number.isFinite(resetEpoch)) { + waitMs = Math.max((resetEpoch * 1000) - Date.now(), 1000); + reason = `x-ratelimit-reset=${resetEpoch}`; + } + + if (waitMs === null && (status === 403 || status === 429)) { + const bodyText = await response.clone().text(); + const mentionsRateLimit = /rate limit|secondary rate limit|abuse/i.test(bodyText); + if (mentionsRateLimit) { + // Secondary rate limiting guidance: start at 1 minute, then exponential backoff. + waitMs = Math.min( + SECONDARY_LIMIT_BASE_WAIT_MS * (2 ** attempt), + SECONDARY_LIMIT_MAX_WAIT_MS + ); + reason = 'secondary-rate-limit-backoff'; + } + } + + if (waitMs !== null) { + if (attempt >= MAX_RATE_LIMIT_RETRIES) { + const exhaustedError = new Error( + `Rate limit retries exhausted for ${label} (status ${status})` + ); + exhaustedError.status = status; + exhaustedError.url = url; + throw exhaustedError; + } + + const jitterMs = Math.floor(Math.random() * 1000); + const totalWaitMs = waitMs + jitterMs; + console.warn( + `Rate limited for ${label} (status ${status}, ${reason}). ` + + `Retrying in ${(totalWaitMs / 1000).toFixed(1)}s ` + + `(attempt ${attempt + 1}/${MAX_RATE_LIMIT_RETRIES})` + ); + await sleep(totalWaitMs); + continue; + } + + return response; + } catch (error) { + lastError = error; + if (attempt >= MAX_RATE_LIMIT_RETRIES) break; + + const backoffMs = Math.min(1000 * (2 ** attempt), 10000); + if (DEBUG) { + console.warn( + `Retrying ${label} (${attempt + 1}/${MAX_RATE_LIMIT_RETRIES}) ` + + `after error: ${error.message}` + ); + } + await sleep(backoffMs); + } + } + + throw lastError; +} + +function toDateTime(value, fallback) { + if (!value) return fallback; + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? fallback : parsed.toISOString(); +} + +function logProgress(message) { + const elapsedSeconds = ((Date.now() - START_TIME) / 1000).toFixed(1); + console.log(`[+${elapsedSeconds}s] ${message}`); +} + +function sanitizeId(id) { + // Vertex AI requires IDs to match [a-zA-Z0-9-_]* + // Replace dots and slashes with hyphens + return id.replace(/[^a-zA-Z0-9-_]/g, '-'); +} + +function makeDoc(id, data) { + const sanitized = sanitizeId(id); + return { + id: sanitized, + structData: { + id: sanitized, + ...data + } + }; +} + +const ID_PATTERN = /^[a-zA-Z0-9-_]*$/; + +function isIsoDateTime(value) { + return typeof value === 'string' && !Number.isNaN(Date.parse(value)); +} + +function normalizeLabel(label) { + if (typeof label === 'string') return label; + if (label && typeof label.name === 'string') return label.name; + return ''; +} + +function extractReferences(content, repo, defaultBranch) { + const text = String(content || ''); + const refs = []; + const seen = new Set(); + + function addRef(rawText, targetUrl) { + if (!rawText || !targetUrl) return; + const key = `${rawText}::${targetUrl}`; + if (seen.has(key)) return; + seen.add(key); + refs.push({ raw_text: rawText, target_url: targetUrl }); + } + + const discussionPattern = /\bdiscussion\s+#(\d+)\b/gi; + for (const match of text.matchAll(discussionPattern)) { + addRef(`#${match[1]}`, `https://github.com/${ORG}/${repo}/discussions/${match[1]}`); + } + + const issuePattern = /\bissues?\s+#(\d+)\b/gi; + for (const match of text.matchAll(issuePattern)) { + addRef(`#${match[1]}`, `https://github.com/${ORG}/${repo}/issues/${match[1]}`); + } + + const prPattern = /\b(?:pr|pull\s*request)\s+#(\d+)\b/gi; + for (const match of text.matchAll(prPattern)) { + addRef(`#${match[1]}`, `https://github.com/${ORG}/${repo}/pull/${match[1]}`); + } + + const blobPathPattern = /\b([\w.-]+\.(?:md|markdown|mdx))\b/gi; + for (const match of text.matchAll(blobPathPattern)) { + addRef(match[1], `https://github.com/${ORG}/${repo}/blob/${defaultBranch}/${match[1]}`); + } + + return refs; +} + +function validateDoc(doc) { + const errors = []; + const data = doc?.structData; + + if (typeof doc.id !== 'string' || doc.id.length === 0 || !ID_PATTERN.test(doc.id)) { + errors.push('invalid id'); + } + + if (!data || typeof data !== 'object' || Array.isArray(data)) { + errors.push('missing structData'); + return { valid: false, errors }; + } + + if (typeof data.id !== 'string' || data.id !== doc.id) { + errors.push('invalid structData.id'); + } + if (typeof data.title !== 'string') errors.push('invalid title'); + if (typeof data.content !== 'string') errors.push('invalid content'); + if (typeof data.url !== 'string') errors.push('invalid url'); + if (typeof data.type !== 'string') errors.push('invalid type'); + if (typeof data.repo !== 'string') errors.push('invalid repo'); + if (typeof data.state !== 'string') errors.push('invalid state'); + if (typeof data.author !== 'string') errors.push('invalid author'); + if (!Array.isArray(data.labels) || data.labels.some(label => typeof label !== 'string')) { + errors.push('invalid labels'); + } + if (!isIsoDateTime(data.created_at)) errors.push('invalid created_at'); + if (!isIsoDateTime(data.updated_at)) errors.push('invalid updated_at'); + if (!Array.isArray(data.references)) { + errors.push('invalid references'); + } else { + for (const ref of data.references) { + if (!ref || typeof ref !== 'object') { + errors.push('invalid reference object'); + break; + } + if (typeof ref.raw_text !== 'string') errors.push('invalid reference raw_text'); + if (typeof ref.target_url !== 'string') errors.push('invalid reference target_url'); + } + } + if (data.number !== undefined && !Number.isInteger(data.number)) { + errors.push('invalid number'); + } + + return { + valid: errors.length === 0, + errors + }; +} + +// Repos to index — add/remove as needed +const REPOS = [ + 'medley', + 'Interlisp.github.io', + 'online', + 'maiko', + 'loops', + 'notecards', + // add others here +]; + +async function githubFetch(url) { + const resp = await githubRequest(url, { headers }, url); + if (!resp.ok) { + const body = await resp.text(); + const error = new Error(`GitHub API error ${resp.status}: ${url} :: ${body.slice(0, 300)}`); + error.status = resp.status; + error.url = url; + throw error; + } + return resp.json(); +} + +function isNotFoundError(error) { + return error && Number(error.status) === 404; +} + +async function getDefaultBranch(repo) { + const meta = await githubFetch(`https://api.github.com/repos/${ORG}/${repo}`); + return { + defaultBranch: meta.default_branch || 'main', + repoUpdatedAt: toDateTime(meta.updated_at, new Date().toISOString()) + }; +} + +async function indexMarkdownFiles(repo, defaultBranch, repoUpdatedAt) { + const docs = []; + const treeResp = await githubFetch( + `https://api.github.com/repos/${ORG}/${repo}/git/trees/${defaultBranch}?recursive=1` + ); + const tree = treeResp.tree || []; + const markdownFiles = tree.filter(entry => + entry.type === 'blob' && + /\.(md|markdown|mdx)$/i.test(entry.path) && + !/^README(\.[^/]+)?$/i.test(path.basename(entry.path)) + ); + + for (const file of markdownFiles) { + try { + const blob = await githubFetch( + `https://api.github.com/repos/${ORG}/${repo}/git/blobs/${file.sha}` + ); + const content = Buffer.from(blob.content || '', 'base64').toString('utf-8'); + const trimmed = content.slice(0, 10000); + const url = `https://github.com/${ORG}/${repo}/blob/${defaultBranch}/${file.path}`; + docs.push(makeDoc( + `gh-markdown-${repo}-${file.path.replace(/\//g, '-')}`, + { + title: `[${repo}] ${file.path}`, + content: trimmed, + url, + references: extractReferences(trimmed, repo, defaultBranch), + type: 'markdown', + labels: [], + repo, + state: 'active', + author: '', + created_at: repoUpdatedAt, + updated_at: repoUpdatedAt + } + )); + } catch (e) { + if (DEBUG) { + console.warn(`Could not index markdown file ${repo}:${file.path}: ${e.message}`); + } + } + } + + return docs; +} + +async function* paginate(url, label = url) { + let nextUrl = url; + let page = 0; + while (nextUrl) { + page += 1; + if (Number.isInteger(MAX_PAGES_PER_PAGINATION) && MAX_PAGES_PER_PAGINATION > 0 && page > MAX_PAGES_PER_PAGINATION) { + console.warn(`Pagination capped for ${label} at ${MAX_PAGES_PER_PAGINATION} pages`); + break; + } + if (DEBUG) { + logProgress(`Fetching ${label} page ${page}`); + } + let resp; + try { + resp = await githubRequest(nextUrl, { headers }, `${label} page ${page}`); + } catch (error) { + console.warn(`Pagination stopped for ${label} at page ${page}: ${error.message}`); + break; + } + if (!resp.ok) { + const body = await resp.text(); + console.warn(`Pagination stopped for ${label} at page ${page}: HTTP ${resp.status} ${body.slice(0, 200)}`); + break; + } + const data = await resp.json(); + yield* data; + + // Parse Link header for next page + const link = resp.headers.get('Link') || ''; + const match = link.match(/<([^>]+)>;\s*rel="next"/); + nextUrl = match ? match[1] : null; + } +} + +function truncateByChars(parts, maxChars) { + const output = []; + let used = 0; + + for (const part of parts) { + if (!part) continue; + const chunk = String(part).trim(); + if (!chunk) continue; + if (used >= maxChars) break; + + const remaining = maxChars - used; + if (chunk.length <= remaining) { + output.push(chunk); + used += chunk.length; + continue; + } + + output.push(chunk.slice(0, remaining)); + used = maxChars; + break; + } + + return output.join('\n\n'); +} + +function promiseWithTimeout(promise, timeoutMs, message) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(message)), timeoutMs); + promise + .then(value => { + clearTimeout(timer); + resolve(value); + }) + .catch(error => { + clearTimeout(timer); + reject(error); + }); + }); +} + +async function fetchPullRequestSearchSignals(repo, prNumber) { + const changedFiles = []; + const commitMessages = []; + + for await (const file of paginate( + `https://api.github.com/repos/${ORG}/${repo}/pulls/${prNumber}/files?per_page=100` + )) { + if (typeof file.filename === 'string' && file.filename.length > 0) { + changedFiles.push(file.filename); + } + } + + for await (const commit of paginate( + `https://api.github.com/repos/${ORG}/${repo}/pulls/${prNumber}/commits?per_page=100` + )) { + const msg = commit.commit?.message; + if (typeof msg === 'string' && msg.length > 0) { + commitMessages.push(msg); + } + } + + return { + changedFiles, + commitMessages + }; +} + +async function indexRepo(repo) { + const docs = []; + logProgress(`Indexing ${ORG}/${repo}...`); + const { defaultBranch, repoUpdatedAt } = await getDefaultBranch(repo); + + logProgress(`${ORG}/${repo}: default branch is ${defaultBranch}`); + + // Index README + try { + logProgress(`${ORG}/${repo}: fetching README`); + const readme = await githubFetch( + `https://api.github.com/repos/${ORG}/${repo}/readme` + ); + const content = Buffer.from(readme.content, 'base64').toString('utf-8'); + const trimmed = content.slice(0, 10000); + const url = `https://github.com/${ORG}/${repo}/blob/${defaultBranch}/${readme.path || 'README.md'}`; + docs.push(makeDoc( + `gh-markdown-${repo}-readme`, + { + title: `${repo} README`, + content: trimmed, + url, + references: extractReferences(trimmed, repo, defaultBranch), + type: 'markdown', + labels: [], + repo, + state: 'active', + author: '', + created_at: repoUpdatedAt, + updated_at: repoUpdatedAt + } + )); + } catch (e) { + if (isNotFoundError(e)) { + console.warn(`No README for ${repo}`); + } else { + console.warn(`README lookup failed for ${repo}: ${e.message}`); + } + } + + // Index all markdown files in the repository (excluding README files). + try { + logProgress(`${ORG}/${repo}: indexing markdown files`); + const markdownDocs = await indexMarkdownFiles(repo, defaultBranch, repoUpdatedAt); + docs.push(...markdownDocs); + logProgress(`${ORG}/${repo}: indexed ${markdownDocs.length} markdown files`); + } catch (e) { + console.warn(`Could not index markdown files for ${repo}: ${e.message}`); + } + + // Index Issues + let issueCount = 0; + for await (const issue of paginate( + `https://api.github.com/repos/${ORG}/${repo}/issues?state=all&per_page=100`, + `${ORG}/${repo} issues` + )) { + if (issue.pull_request) continue; // skip PRs here, handle separately + issueCount += 1; + const content = (issue.body || '').slice(0, 10000); + docs.push(makeDoc( + `gh-issue-${repo}-${issue.number}`, + { + title: issue.title, + content, + url: issue.html_url, + references: extractReferences(content, repo, defaultBranch), + type: 'issue', + labels: (issue.labels || []).map(normalizeLabel).filter(Boolean), + repo, + state: issue.state, + author: issue.user?.login || '', + created_at: toDateTime(issue.created_at, repoUpdatedAt), + updated_at: toDateTime(issue.updated_at, repoUpdatedAt), + number: issue.number + } + )); + + if (issueCount === 1 || issueCount % 25 === 0) { + logProgress(`${ORG}/${repo}: indexed ${issueCount} issues`); + } + } + + logProgress(`${ORG}/${repo}: finished issues (${issueCount})`); + + // Index Pull Requests + let prCount = 0; + for await (const pr of paginate( + `https://api.github.com/repos/${ORG}/${repo}/pulls?state=all&per_page=100`, + `${ORG}/${repo} pull requests` + )) { + prCount += 1; + if (prCount === 1 || prCount % 10 === 0) { + logProgress(`${ORG}/${repo}: processing PR ${prCount} (#${pr.number})`); + } + + let prSignals = { changedFiles: [], commitMessages: [] }; + + try { + prSignals = await promiseWithTimeout( + fetchPullRequestSearchSignals(repo, pr.number), + 20000, + `Timed out fetching PR details for ${repo}#${pr.number}` + ); + } catch (e) { + if (DEBUG) { + console.warn(`Could not fetch PR details ${repo}#${pr.number}: ${e.message}`); + } + } + + const content = truncateByChars([ + pr.body || '', + prSignals.changedFiles.length > 0 + ? `Changed files:\n${prSignals.changedFiles.join('\n')}` + : '', + prSignals.commitMessages.length > 0 + ? `Commit messages:\n${prSignals.commitMessages.join('\n---\n')}` + : '' + ], 10000); + + docs.push(makeDoc( + `gh-pr-${repo}-${pr.number}`, + { + title: pr.title, + content, + url: pr.html_url, + references: extractReferences(content, repo, defaultBranch), + type: 'pull_request', + labels: (pr.labels || []).map(normalizeLabel).filter(Boolean), + repo, + state: pr.state, + author: pr.user?.login || '', + created_at: toDateTime(pr.created_at, repoUpdatedAt), + updated_at: toDateTime(pr.updated_at, repoUpdatedAt), + number: pr.number + } + )); + } + + logProgress(`${ORG}/${repo}: finished pull requests (${prCount})`); + + // Index Discussions (requires GraphQL API) + try { + logProgress(`${ORG}/${repo}: fetching discussions`); + const discussionsResp = await githubRequest('https://api.github.com/graphql', { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: ` + query { + repository(owner: "${ORG}", name: "${repo}") { + discussions(first: 100) { + nodes { + number + title + body + url + createdAt + updatedAt + author { login } + category { name } + } + } + } + } + `}) + }); + + const discussionsData = await discussionsResp.json(); + const discussions = discussionsData.data?.repository?.discussions?.nodes || []; + logProgress(`${ORG}/${repo}: found ${discussions.length} discussions`); + + discussions.forEach(d => { + const content = (d.body || '').slice(0, 10000); + docs.push(makeDoc( + `gh-discussion-${repo}-${d.number}`, + { + title: d.title, + content, + url: d.url, + references: extractReferences(content, repo, defaultBranch), + type: 'discussion', + labels: d.category?.name ? [d.category.name] : [], + repo, + state: 'open', + author: d.author?.login || '', + created_at: toDateTime(d.createdAt, repoUpdatedAt), + updated_at: toDateTime(d.updatedAt, repoUpdatedAt), + number: d.number + } + )); + }); + } catch (e) { + console.warn(`Could not fetch discussions for ${repo}:`, e.message); + } + + return docs; +} + +async function main() { + const allDocs = []; + + for (const repo of REPOS) { + try { + logProgress(`Starting repo ${ORG}/${repo}`); + const docs = await indexRepo(repo); + allDocs.push(...docs); + logProgress(`${ORG}/${repo}: produced ${docs.length} documents`); + } catch (e) { + console.error(`Failed to index ${repo}:`, e.message); + } + } + + const validDocs = []; + let invalidCount = 0; + + for (const doc of allDocs) { + const result = validateDoc(doc); + if (result.valid) { + validDocs.push(doc); + continue; + } + + invalidCount += 1; + console.warn( + `Skipping invalid document ${doc.id || ''}: ${result.errors.join(', ')}` + ); + } + + const jsonl = validDocs.map(d => JSON.stringify(d)).join('\n'); + fs.writeFileSync(OUTPUT_FILE, jsonl); + logProgress(`Wrote ${validDocs.length} valid documents to ${OUTPUT_FILE}`); + if (invalidCount > 0) { + console.warn(`Skipped ${invalidCount} invalid documents during pre-write validation`); + } +} + +//main().catch(console.error); +main(); diff --git a/search-function/index.js b/search-function/index.js index 31133575..6436dc24 100644 --- a/search-function/index.js +++ b/search-function/index.js @@ -100,18 +100,28 @@ exports.search = async (req, res) => { const results = (data.results || []).map(result => { const derived = result.document?.derivedStructData; - if (!derived) return null; - - const url = derived.link || null; - const snippet = derived.snippets?.[0]?.snippet || null; - // Strip angle brackets to prevent HTML/script tag injection in display text - const stripHtml = (str) => str ? str.replace(/[<>]/g, '') : null; - + const struct = result.document?.structData; + + // Some documents expose URL as derivedStructData.link, others as derivedStructData.url. + // Support both so GitHub issues/PRs/discussions are surfaced as clickable results. + const url = derived?.link || derived?.url || struct?.url || null; + + // title: website crawl uses derivedStructData.title; structured docs use keyPropertyMapping:"title" + // which also maps to derivedStructData.title — fall back to structData.title if missing. + const title = derived?.title || struct?.title || null; + + // snippets: generated from content field when keyPropertyMapping:"body" is set in the schema. + // Falls back to structData.content substring for structured docs without body mapping. + const rawSnippet = derived?.snippets?.[0]?.snippet || struct?.content?.slice(0, 300) || null; + return { id: result.document?.id, - title: derived.title || null, + title, url, - snippet: stripHtml(derived.snippets?.[0]?.snippet || null), + snippet: rawSnippet, + type: struct?.type || null, + repo: struct?.repo || null, + state: struct?.state || null, section: url?.replace('https://interlisp.org/', '')?.split('/')?.[0] || '', }; }).filter(r => r?.url); @@ -120,7 +130,8 @@ exports.search = async (req, res) => { const docIdToUrl = {}; (data.results || []).forEach(result => { const id = result.document?.id; - const url = result.document?.derivedStructData?.link; + const derived = result.document?.derivedStructData; + const url = derived?.link || derived?.url || result.document?.structData?.url; if (id && url) docIdToUrl[id] = url; }); @@ -152,7 +163,10 @@ exports.search = async (req, res) => { }; function buildPreamble(context) { - const base = `You are a search assistant for the Interlisp documentation site. + const base = `You are a search assistant for the Interlisp site. You answer questions about documentation, code examples, and historical information related to Interlisp. Use the search results to provide accurate and concise answers. +If the user query is about a specific section of the site, prioritize information from that section in your response. +If the question is about code, provide code snippets where relevant. Be sure to distinguish between different versions of Interlisp or Common Lisp. +If the question is related to maintaining and modernizing Interlisp, include information from the GitHub site, its Issues, Discussions and Pull Requests. Answer in strict Markdown only. Use this structure exactly when applicable: - One short opening paragraph.