From ea663c2ff5f67ec9b0ab9da10ef3397fbf39e0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Wed, 27 Aug 2025 21:21:50 -0400 Subject: [PATCH 1/5] adding hub-sync feature code --- .env.example | 24 + docs/hubSyncHandler/README.md | 34 + index.js | 115 +- lib/env.js | 6 + lib/hubSyncHandler.js | 258 + lib/installationCache.js | 93 + lib/routes.js | 483 ++ package-lock.json | 792 ++- package.json | 1 + ui/.eslintrc.json | 3 + ui/.gitignore | 40 + ui/README.md | 40 + ui/favicon.ico | Bin 0 -> 3872 bytes ui/next.config.js | 10 + ui/package-lock.json | 5059 +++++++++++++++++ ui/package.json | 23 + ui/public/favicon.svg | 3 + ui/public/shield.png | Bin 0 -> 24289 bytes ui/shield.png | Bin 0 -> 3872 bytes ui/src/app/[slug]/route.js | 18 + ui/src/app/components/EnvVariables.jsx | 161 + ui/src/app/components/OrganizationsTable.jsx | 252 + .../components/Safe-settings-hubContent.jsx | 412 ++ ui/src/app/components/ThemeContext.jsx | 71 + ui/src/app/components/ThemeToggle.jsx | 17 + ui/src/app/components/TitleBar.css | 174 + ui/src/app/components/TitleBar.jsx | 84 + ui/src/app/dashboard/env/page.jsx | 23 + ui/src/app/dashboard/organizations/page.jsx | 24 + ui/src/app/dashboard/page.jsx | 13 + .../app/dashboard/safe-settings-hub/page.jsx | 25 + ui/src/app/dashboard/settings/page.jsx | 13 + ui/src/app/globals.css | 260 + ui/src/app/hooks/useClientSafe.js | 45 + ui/src/app/hooks/useHydrated.js | 18 + ui/src/app/layout.jsx | 41 + ui/src/app/not-found.jsx | 15 + ui/src/app/route.js | 7 + 38 files changed, 8290 insertions(+), 367 deletions(-) create mode 100644 docs/hubSyncHandler/README.md create mode 100644 lib/hubSyncHandler.js create mode 100644 lib/installationCache.js create mode 100644 lib/routes.js create mode 100644 ui/.eslintrc.json create mode 100644 ui/.gitignore create mode 100644 ui/README.md create mode 100644 ui/favicon.ico create mode 100644 ui/next.config.js create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/public/favicon.svg create mode 100644 ui/public/shield.png create mode 100644 ui/shield.png create mode 100644 ui/src/app/[slug]/route.js create mode 100644 ui/src/app/components/EnvVariables.jsx create mode 100644 ui/src/app/components/OrganizationsTable.jsx create mode 100644 ui/src/app/components/Safe-settings-hubContent.jsx create mode 100644 ui/src/app/components/ThemeContext.jsx create mode 100644 ui/src/app/components/ThemeToggle.jsx create mode 100644 ui/src/app/components/TitleBar.css create mode 100644 ui/src/app/components/TitleBar.jsx create mode 100644 ui/src/app/dashboard/env/page.jsx create mode 100644 ui/src/app/dashboard/organizations/page.jsx create mode 100644 ui/src/app/dashboard/page.jsx create mode 100644 ui/src/app/dashboard/safe-settings-hub/page.jsx create mode 100644 ui/src/app/dashboard/settings/page.jsx create mode 100644 ui/src/app/globals.css create mode 100644 ui/src/app/hooks/useClientSafe.js create mode 100644 ui/src/app/hooks/useHydrated.js create mode 100644 ui/src/app/layout.jsx create mode 100644 ui/src/app/not-found.jsx create mode 100644 ui/src/app/route.js diff --git a/.env.example b/.env.example index d98f7d4b9..7f8a45ee3 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,27 @@ # Uncomment this to get GitHub comments for the Pull Request Workflow. # ENABLE_PR_COMMENT=true + +# ADMIN_REPO=safe-settings-config +CONFIG_PATH=.github +SETTINGS_FILE_PATH=settings.yml + +# Configuration support for Hub-Sync safe-settings feature +# SAFE_SETTINGS_HUB_REPO=safe-settings-config-master +# SAFE_SETTINGS_HUB_ORG=foo-training +# A subfolder under 'CONFIG_PATH' where the 'organizations//' structure is found +# SAFE_SETTINGS_HUB_PATH=safe-settings +# SAFE_SETTINGS_HUB_DIRECT_PUSH=true + + + +# ┌────────────── second (optional) +# │ ┌──────────── minute +# │ │ ┌────────── hour +# │ │ │ ┌──────── day of month +# │ │ │ │ ┌────── month +# │ │ │ │ │ ┌──── day of week +# │ │ │ │ │ │ +# │ │ │ │ │ │ +# * * * * * * +# CRON=* * * * * # Run every minute \ No newline at end of file diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md new file mode 100644 index 000000000..7ffd23ab3 --- /dev/null +++ b/docs/hubSyncHandler/README.md @@ -0,0 +1,34 @@ +# Safe Settings Organization Sync & Dashboard + + This feature provides a centralized approach to managing the Safe-Settings Admin Repo, allowing Safe-Settings configurations to be sync'd across multiple ORGs. + +## Overview + +This feature adds a hub‑and‑spoke synchronization capability to Safe Settings. + +One central **master admin repository** (the hub) serves as the authoritative source of configuration which is automatically propagated to each organization’s **admin repository** (the spokes). + +**Note:** When something changes in the central repo, only those changed files are copied to each affected ORG’s admin repo, so everything stays in sync with little manual work. + +## Sync Lifecycle (High Level) + +```mermaid +graph TD +A0(PR Closed) --> A1(HUB Admin Repo) +A1(ORG Admin Repo) --> B(ORG Admin Repo) +A1(HUB Admin Repo) --> C(ORG Admin Repo) +A1(HUB Admin Repo) --> D(ORG Admin Repo) +``` + +## Environment Variables & Inputs + +Environment variables specific to the 'Sync-Feature' + +| Name | Purpose | Default | +|------|---------|---------| +| `SAFE_SETTINGS_HUB_REPO` | Repo for master safe-settings contents | admin-master | +| `SAFE_SETTINGS_HUB_ORG` | Organization that hold the Repo | admin-master-org | +| `SAFE_SETTINGS_HUB_PATH` | source folder | .github/safe-settings | +| `SAFE_SETTINGS_HUB_DIRECT_PUSH` | Use a PR or direct commit | false | + + diff --git a/index.js b/index.js index f6af26b5a..e4c809f66 100644 --- a/index.js +++ b/index.js @@ -6,91 +6,22 @@ const Glob = require('./lib/glob') const ConfigManager = require('./lib/configManager') const NopCommand = require('./lib/nopcommand') const env = require('./lib/env') +const { setupRoutes } = require('./lib/routes') +const { initCache } = require('./lib/installationCache') +const { hubSyncHandler } = require('./lib/hubSyncHandler') let deploymentConfig module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => { let appSlug = 'safe-settings' - async function syncAllSettings (nop, context, repo = context.repo(), ref) { - try { - deploymentConfig = await loadYamlFileSystem() - robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) - const configManager = new ConfigManager(context, ref) - const runtimeConfig = await configManager.loadGlobalSettingsYaml() - const config = Object.assign({}, deploymentConfig, runtimeConfig) - robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - if (ref) { - return Settings.syncAll(nop, context, repo, config, ref) - } else { - return Settings.syncAll(nop, context, repo, config) - } - } catch (e) { - if (nop) { - let filename = env.SETTINGS_FILE_PATH - if (!deploymentConfig) { - filename = env.DEPLOYMENT_CONFIG_FILE_PATH - deploymentConfig = {} - } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') - robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) - } else { - throw e - } - } - } - async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { - try { - deploymentConfig = await loadYamlFileSystem() - robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) - const configManager = new ConfigManager(context, ref) - const runtimeConfig = await configManager.loadGlobalSettingsYaml() - const config = Object.assign({}, deploymentConfig, runtimeConfig) - robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref) - } catch (e) { - if (nop) { - let filename = env.SETTINGS_FILE_PATH - if (!deploymentConfig) { - filename = env.DEPLOYMENT_CONFIG_FILE_PATH - deploymentConfig = {} - } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') - robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) - } else { - throw e - } - } - } + // Initialize all routes (static UI + API) via centralized module + setupRoutes(robot, getRouter) - async function syncSettings (nop, context, repo = context.repo(), ref) { - try { - deploymentConfig = await loadYamlFileSystem() - robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) - const configManager = new ConfigManager(context, ref) - const runtimeConfig = await configManager.loadGlobalSettingsYaml() - const config = Object.assign({}, deploymentConfig, runtimeConfig) - robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.sync(nop, context, repo, config, ref) - } catch (e) { - if (nop) { - let filename = env.SETTINGS_FILE_PATH - if (!deploymentConfig) { - filename = env.DEPLOYMENT_CONFIG_FILE_PATH - deploymentConfig = {} - } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') - robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) - } else { - throw e - } - } - } + // Initialize installation cache (env-controlled prefetch) + initCache(robot) - async function renameSync (nop, context, repo = context.repo(), rename, ref) { + async function renameSync(nop, context, repo = context.repo(), rename, ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -115,13 +46,14 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } } + /** * Loads the deployment config file from file system * Do this once when the app starts and then return the cached value * * @return The parsed YAML file */ - async function loadYamlFileSystem () { + async function loadYamlFileSystem() { if (deploymentConfig === undefined) { const deploymentConfigPath = env.DEPLOYMENT_CONFIG_FILE_PATH if (fs.existsSync(deploymentConfigPath)) { @@ -133,7 +65,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return deploymentConfig } - function getAllChangedSubOrgConfigs (payload) { + function getAllChangedSubOrgConfigs(payload) { const pattern = Settings.SUB_ORG_PATTERN const getMatchingFiles = (commits, type) => @@ -150,7 +82,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getAllChangedRepoConfigs (payload, owner) { + function getAllChangedRepoConfigs(payload, owner) { const pattern = Settings.REPO_PATTERN const getMatchingFiles = (commits, type) => @@ -167,7 +99,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedRepoConfigName (files, owner) { + function getChangedRepoConfigName(files, owner) { const pattern = Settings.REPO_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -178,7 +110,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedSubOrgConfigName (files) { + function getChangedSubOrgConfigName(files) { const pattern = Settings.SUB_ORG_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -188,7 +120,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => path: modifiedFile })) } - async function createCheckRun (context, pull_request, head_sha, head_branch) { + async function createCheckRun(context, pull_request, head_sha, head_branch) { const { payload } = context // robot.log.debug(`Check suite was requested! for ${context.repo()} ${pull_request.number} ${head_sha} ${head_branch}`) const res = await context.octokit.checks.create({ @@ -200,7 +132,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => robot.log.debug(JSON.stringify(res, null)) } - async function info () { + async function info() { const github = await robot.auth() const installations = await github.paginate( github.apps.listInstallations.endpoint.merge({ per_page: 100 }) @@ -215,7 +147,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncInstallation (nop = false) { + async function syncInstallation(nop = false) { robot.log.trace('Fetching installations') const github = await robot.auth() @@ -521,6 +453,19 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return createCheckRun(context, pull_request, payload.pull_request.head.sha, payload.pull_request.head.ref) }) + /** + * @description Handle pull_request.closed events to support hub synchronization + * @param {Object} context - The context object provided by Probot + */ + robot.on('pull_request.closed', async context => { + try { + await hubSyncHandler(robot, context) + } catch (err) { + robot.log.error(`pull_request.closed handler failed: ${err && err.message ? err.message : err}`) + } + return null + }) + robot.on(['check_suite.rerequested'], async context => { robot.log.debug('Check suite was rerequested!') return createCheckRun(context) diff --git a/lib/env.js b/lib/env.js index 94c0ea742..8bea61a73 100644 --- a/lib/env.js +++ b/lib/env.js @@ -1,5 +1,11 @@ module.exports = { ADMIN_REPO: process.env.ADMIN_REPO || 'admin', + SAFE_SETTINGS_HUB_REPO: process.env.SAFE_SETTINGS_HUB_REPO || 'admin-master', + SAFE_SETTINGS_HUB_ORG: process.env.SAFE_SETTINGS_HUB_ORG || 'admin-master-org', + SAFE_SETTINGS_HUB_DIRECT_PUSH: process.env.SAFE_SETTINGS_HUB_DIRECT_PUSH || 'false', + SAFE_SETTINGS_HUB_PATH: process.env.SAFE_SETTINGS_HUB_PATH || '.github/safe-settings', + APP_ID: process.env.APP_ID || null, + PRIVATE_KEY_PATH: process.env.PRIVATE_KEY_PATH || 'private-key.pem', CONFIG_PATH: process.env.CONFIG_PATH || '.github', SETTINGS_FILE_PATH: process.env.SETTINGS_FILE_PATH || 'settings.yml', DEPLOYMENT_CONFIG_FILE_PATH: process.env.DEPLOYMENT_CONFIG_FILE || 'deployment-settings.yml', diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js new file mode 100644 index 000000000..c34de6d66 --- /dev/null +++ b/lib/hubSyncHandler.js @@ -0,0 +1,258 @@ +const env = require('./env') +const { getInstallations } = require('./installationCache') + +/** + * Sync changed safe-settings organization files from the master admin PR + * into the target organization's admin repository. + * @param {import('probot').Probot} robot + * @param {import('probot').Context} context + * @param {string} orgName Destination organization login (also folder name under organizations/) + * @param {string} destRepo Destination repo name inside orgName (e.g. admin repo) + * @param {string} destinationFolder Base folder in destination repo where content lives (e.g. .github or .github/safe-settings) + */ +async function syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder) { + try { + robot.log.info(`Syncing safe settings for organization: ${orgName}`); + + robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`); + const pr = context.payload.pull_request; + if (!pr) { + robot.log.warn('No pull_request payload found; aborting sync'); + return; + } + const { owner: srcOwner, repo: srcRepo } = context.repo(); + const pull_number = pr.number; + + // Source base path where org folders live inside master admin repo + + // 'safe-settings' is the standard sub-folder path + const configRoot = env.CONFIG_PATH || '.github/'; + const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, ''); + robot.log.info(`DEBUG: sourceBase='${sourceBase}'`); + + // Debug info: log env and computed paths + robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`); + + // List changed files in PR + const files = await context.octokit.paginate( + context.octokit.rest.pulls.listFiles, + { owner: srcOwner, repo: srcRepo, pull_number, per_page: 100 } + ); + + robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`); + if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); + + // Dump file objects for debugging filename issues + if (files.length) { + try { + robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`); + robot.log.info(`DEBUG: file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`); + } catch (e) { + robot.log.info(`DEBUG: failed to stringify first file: ${e.message}`); + } + files.forEach((f, i) => { + try { + robot.log.info(`DEBUG: FILE[${i}] raw=${JSON.stringify(f)}`); + robot.log.info(`DEBUG: FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`); + } catch (e) { + robot.log.info(`DEBUG: FILE[${i}] stringify error: ${e.message}`); + } + }); + } + + const orgPrefix = `${sourceBase}/${orgName}/`; + robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); + robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`); + const relevant = files.filter(f => f.filename === `${sourceBase}/${orgName}` || f.filename.startsWith(orgPrefix)); + robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`); + if (!relevant.length) { + robot.log.info(`No files for org ${orgName} in PR #${pull_number}`); + // Detailed per-file checks to help debug matching + files.forEach(f => { + const exact = f.filename === `${sourceBase}/${orgName}`; + const pref = f.filename.startsWith(orgPrefix); + robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`); + }); + // Also show alternate check using CONFIG_PATH + '/organizations' + const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations`; + const altPrefix = `${altBase}/${orgName}/`; + files.forEach(f => { + const exactAlt = f.filename === `${altBase}/${orgName}`; + const prefAlt = f.filename.startsWith(altPrefix); + robot.log.info(`ALT CHECK: file='${f.filename}' exactAlt=${exactAlt} prefAlt=${prefAlt}`); + }); + return; + } + + // Destination info + const destOwner = orgName; + // ensure destBase uses the configured CONFIG_PATH (fallback to '.github') and normalize trailing slash + const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, ''); + const destBaseBranch = 'main'; + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1'); + + // Find installation for destination org to auth + const installs = await getInstallations(robot) + const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === destOwner.toLowerCase()); + if (!install) { + robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`); + return; + } + const githubDest = await robot.auth(install.id); + + robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`); + + // Create branch if not direct push + const timestamp = Date.now(); + const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}`; + if (!directPush) { + try { + const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }); + const baseSha = baseRef.data.object.sha; + await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }); + robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`); + } catch (err) { + if (err.status === 422) { + robot.log.warn(`Branch ${branchName} already exists, continuing`); + } else { + throw err; + } + } + } + + for (const f of relevant) { + let relative; + if (f.filename === `${sourceBase}/${orgName}`) { + // top directory marker encountered (unlikely in changed files list) - skip + continue; + } else { + relative = f.filename.slice(orgPrefix.length); + } + // place only the changed file under the configured CONFIG_PATH (e.g. '.github/') + const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/'); + try { + const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }); + const data = srcContentResp.data; + if (Array.isArray(data)) { + // Skip directories; individual files will appear separately in changed files list + continue; + } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8'); + const encoded = Buffer.from(fileContent, 'utf8').toString('base64'); + + // Check existing file for sha + let existingSha = undefined; + try { + const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }); + if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha; + } catch (getErr) { + if (getErr.status !== 404) throw getErr; // ignore missing + } + + await githubDest.rest.repos.createOrUpdateFileContents({ + owner: destOwner, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, + content: encoded, + branch: branchName, + sha: existingSha, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }); + robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`); + } catch (fileErr) { + robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`); + throw fileErr; + } + } + + if (!directPush) { + try { + const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`; + const prBody = `Automated sync of safe-settings for ${orgName} from ${srcOwner}/${srcRepo} PR #${pull_number}.`; + const created = await githubDest.rest.pulls.create({ owner: destOwner, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }); + robot.log.info(`Created PR ${created.data.html_url} in ${destOwner}/${destRepo}`); + } catch (prErr) { + robot.log.error(`Failed to create PR in ${destOwner}/${destRepo}: ${prErr.message}`); + throw prErr; + } + } else { + robot.log.info(`Changes pushed directly to ${destOwner}/${destRepo}@${destBaseBranch}`); + } + } catch (err) { + robot.log.error(`syncSafeSettingConfig error for org ${orgName}: ${err.message}`); + } +} + +/** + * Handle closed pull requests to sync safe-settings changes to target organizations. + * Focus on the organization and repository specified in the pull request and if they belong to the Safe-Settings Hub. + * @param {import('probot').Probot} robot + * @param {import('probot').Context} context + */ +async function hubSyncHandler(robot, context) { + const { payload } = context; + const { repository, pull_request } = payload || {}; + robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`); + try { + // Ensure the event is from the configured Safe-Settings Hub repo/org + const isMasterRepo = repository && repository.name === env.SAFE_SETTINGS_HUB_REPO; + const isMasterOrg = repository && repository.owner && repository.owner.login === env.SAFE_SETTINGS_HUB_ORG; + + if (!(isMasterRepo && isMasterOrg)) { + robot.log.info(`Pull request.closed is not from master admin repo/org (${env.SAFE_SETTINGS_HUB_ORG}/${env.SAFE_SETTINGS_HUB_REPO}), ignoring`); + return; + } + + robot.log.info(`Pull request closed on Safe-Settings Hub: (${repository.full_name})`); + + // Get the PR details + const pr = pull_request; + const { owner, repo } = context.repo(); + const pull_number = pr.number; + const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`; + + // Paginate through all files changed in the PR + const files = await context.octokit.paginate( + context.octokit.rest.pulls.listFiles, + { owner, repo, pull_number, per_page: 100 } + ); + + robot.log.info(`Files changed in PR #${pull_number}: ${files.map(f => f.filename).join(', ')}`); + + // Normalize baseSettingsPath (remove trailing slash if any) + const normalizedBase = baseSettingsPath.replace(/\/$/, ''); + robot.log.debug(`Normalized base path: ${normalizedBase}`); + + // Escape string for use in RegExp + const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // Build a RegExp that captures the first path segment after the base path + const basePattern = new RegExp(`^${escapeRegex(normalizedBase)}/([^/]+)(?:/|$)`); + robot.log.debug(`Base pattern for org matching: ${basePattern}`); + + // Collect unique org names + const orgNamesSet = new Set(); + files.forEach(f => { + const m = f.filename.match(basePattern); + if (m && m[1]) { + orgNamesSet.add(m[1]); + } + }); + + const orgNames = Array.from(orgNamesSet); // e.g. ['jester-lab', 'jefeish'] + robot.log.info(`Orgs updated in PR #${pull_number}: ${orgNames.join(', ')}`); + + // Iterate over each updated org and sync settings + for (const orgName of orgNames) { + const destRepo = env.ADMIN_REPO; + const destinationFolder = env.CONFIG_PATH || '.github'; + await syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder); + } + } catch (err) { + robot.log.error(`Failed to sync safe settings: ${err && err.message ? err.message : err}`); + } +} + +module.exports = { hubSyncHandler }; \ No newline at end of file diff --git a/lib/installationCache.js b/lib/installationCache.js new file mode 100644 index 000000000..fcfb2a75c --- /dev/null +++ b/lib/installationCache.js @@ -0,0 +1,93 @@ +// Installation cache with TTL for GitHub App installations. +// Provides a hybrid approach: live refresh when stale, fast reads otherwise. + +let cachedInstallations = [] +let cachedOrgLogins = [] +let lastFetchedAt = null +let inFlightPromise = null + +const DEFAULT_TTL_MS = 60_000 +function getTtlMs() { + const v = parseInt(process.env.INSTALLATION_CACHE_TTL_MS, 10) + return isNaN(v) || v < 5_000 ? DEFAULT_TTL_MS : v +} + +async function fetchInstallations(robot, { perPage = 100 } = {}) { + const github = await robot.auth() + return github.paginate( + github.apps.listInstallations.endpoint.merge({ per_page: perPage }) + ) +} + +async function refresh(robot, opts = {}) { + if (inFlightPromise) return inFlightPromise + inFlightPromise = (async () => { + try { + const installs = await fetchInstallations(robot, opts) + cachedInstallations = installs + cachedOrgLogins = installs + .filter(i => i.account && i.account.type === 'Organization') + .map(i => i.account.login) + .sort() + lastFetchedAt = new Date() + } catch (e) { + robot.log && robot.log.warn && robot.log.warn(`Installation cache refresh failed: ${e.message}`) + throw e + } finally { + inFlightPromise = null + } + return cachedInstallations + })() + return inFlightPromise +} + +function startPrefetch(robot, opts = {}) { + return refresh(robot, opts) +} + +/** + * Initialize cache (always prefetch once at startup) and log result. + */ +function initCache(robot) { + return startPrefetch(robot) + .then(installs => { + robot.log && robot.log.info && robot.log.info(`Installation cache prefetched ${installs.length} installs (${cachedOrgLogins.length} orgs) [TTL=${getTtlMs()}ms]`) + return true + }) + .catch(e => { + robot.log && robot.log.warn && robot.log.warn(`Installation cache prefetch failed: ${e.message}`) + return false + }) +} + +async function ensureFresh(robot) { + const ttl = getTtlMs() + if (!lastFetchedAt || (Date.now() - lastFetchedAt.getTime()) > ttl) { + try { await refresh(robot) } catch (_) { /* stale ok */ } + } +} + +async function getInstallations(robot) { + await ensureFresh(robot) + return cachedInstallations.slice() +} + +function getOrgLogins() { return cachedOrgLogins.slice() } +function getLastFetchedAt() { return lastFetchedAt } + +// Test-only helper: force cache to appear stale on next access +function __forceStale() { + lastFetchedAt = new Date(Date.now() - (getTtlMs() + 10_000)) +} + +module.exports = { + startPrefetch, + initCache, + refresh, + getInstallations, + getOrgLogins, + getLastFetchedAt, + // for tests / diagnostics + _debug: () => ({ size: cachedInstallations.length, lastFetchedAt }), + __forceStale +} diff --git a/lib/routes.js b/lib/routes.js new file mode 100644 index 000000000..7bbd0e572 --- /dev/null +++ b/lib/routes.js @@ -0,0 +1,483 @@ +/** + * Router setup for Safe Settings UI & API endpoints + * Centralizes Express/Next asset & API wiring away from core app logic. + * + * Exports: + * setupRoutes(robot, getRouter) -> configured router + * + * Responsibilities: + * - Serve static exported Next.js UI (from ui/out) + * - Dashboard HTML entry points + * - JSON API endpoints + * + * This version removes dependency on robot-level cached installation getters + * (`robot.getCachedInstallations`, `robot.getOrganizationLogins`) and instead + * fetches installations live per request. If performance becomes an issue, + * a lightweight in-module memoization layer with short TTL can be reintroduced. + */ + +const path = require('path') +const fs = require('fs') +const express = require('express') +const env = require('./env') +const { getInstallations: cacheGetInstallations, getOrgLogins, getLastFetchedAt } = require('./installationCache') + +// Lightweight commit metadata cache (path+ref -> meta) with TTL to avoid +// repeated GitHub commit lookups across requests. +const COMMIT_META_TTL_MS = parseInt(process.env.COMMIT_META_TTL_MS || '300000') // 5m default +const _commitMetaCache = new Map() // key => { meta, expiresAt } +function getCachedCommitMeta(key) { + const entry = _commitMetaCache.get(key) + if (!entry) return null + if (Date.now() > entry.expiresAt) { _commitMetaCache.delete(key); return null } + return entry.meta +} +function setCachedCommitMeta(key, meta) { + _commitMetaCache.set(key, { meta, expiresAt: Date.now() + COMMIT_META_TTL_MS }) +} + +function setupRoutes(robot, getRouter) { + // Root-level mount (can be changed to '/dashboard' if desired) + const router = getRouter('/') + + // Static assets: produced by Next export/build step (ui/out) + const rootDir = path.join(__dirname, '..') // lib -> project root + const uiPath = path.join(rootDir, 'ui', 'out') + router.use(express.static(uiPath)) + + // HTML entrypoints (exported files). Adjust if you move/rename pages. + router.get('/dashboard', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard.html')) + }) + + router.get('/dashboard/organizations', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'organizations.html')) + }) + + router.get('/dashboard/settings', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'settings.html')) + }) + + router.get('/dashboard/safe-settings-hub', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'safe-settings-hub.html')) + }) + + router.get('/dashboard/env', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'env.html')) + }) + + // Apple touch icon (silence 404s). Replace file logic if you add a real 180x180 asset. + const APPLE_TOUCH_ICON_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAQAAAA9zQYyAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==' // 180x180 transparent PNG + router.get('/apple-touch-icon.png', (req, res) => { + // If a real file exists at project root, serve it; otherwise fallback to embedded transparent PNG. + const filePath = path.join(rootDir, 'apple-touch-icon.png') + fs.access(filePath, fs.constants.R_OK, (err) => { + if (!err) { + return res.sendFile(filePath) + } + const buf = Buffer.from(APPLE_TOUCH_ICON_BASE64, 'base64') + res.setHeader('Content-Type', 'image/png') + res.setHeader('Cache-Control', 'public, max-age=86400, immutable') + res.send(buf) + }) + }) + + /** + * GET /api/organizations + * Returns live organization installation metadata + optional last commit info. + * Query param: disableActivity=true to skip commit lookups (faster). + */ + router.get('/api/organizations', async (req, res) => { + const disableActivity = req.query.disableActivity === 'true' + const includeActivity = !disableActivity + try { + const installs = await cacheGetInstallations(robot) + const orgLogins = getOrgLogins() + const orgInstalls = installs.filter(i => i.account && i.account.type === 'Organization') + const installationDtos = orgInstalls.map(i => ({ id: i.id, account: i.account.login, type: i.account.type, created_at: i.created_at })) + + const lastCommits = {} + if (includeActivity) { + const adminRepoName = env.ADMIN_REPO + if (adminRepoName) { + try { + const orgs = orgLogins + const limit = 5 + const queue = [...orgs] + const runners = [] + const runNext = async () => { + while (queue.length) { + const org = queue.shift() + try { + const install = installs.find(i => i.account && i.account.login.toLowerCase() === org.toLowerCase()) + if (!install) { + lastCommits[org] = { na: true } + continue + } + const githubOrg = await robot.auth(install.id) + const pathPrefix = `${env.CONFIG_PATH.replace(/\/$/, '')}/organizations/${org}` + let commits + try { + commits = await githubOrg.repos.listCommits({ owner: org, repo: adminRepoName, per_page: 1, path: pathPrefix }) + } catch (err) { + if (err.status === 404) { + // Repo or path not found -> NA for repository + lastCommits[org] = { na: true } + continue + } + if (err.status === 409) { // empty repo + lastCommits[org] = null + continue + } + robot.log && robot.log.warn && robot.log.warn(`Commit lookup error for ${org}/${adminRepoName}: ${err.message}`) + lastCommits[org] = null + continue + } + if (Array.isArray(commits.data) && commits.data.length) { + const c = commits.data[0] + const committedAt = (c.commit && c.commit.author && c.commit.author.date) || null + const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null + lastCommits[org] = { sha: c.sha, committed_at: committedAt, message: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, age_seconds: ageSeconds } + } else { + lastCommits[org] = null + } + } catch (loopErr) { + robot.log && robot.log.warn && robot.log.warn(`Unexpected error gathering commit for org ${org}: ${loopErr.message}`) + lastCommits[org] = null + } + } + } + for (let i = 0; i < limit; i++) runners.push(runNext()) + await Promise.all(runners) + } catch (activityErr) { + // On failure mark all orgs as NA and log warning + orgLogins.forEach(o => { lastCommits[o] = { na: true } }) + robot.log && robot.log.warn && robot.log.warn(`Failed gathering last commit activity: ${activityErr.message}`) + } + } else { + orgLogins.forEach(o => { lastCommits[o] = { na: true } }) + } + } + + return res.json({ updatedAt: new Date().toISOString(), organizations: orgLogins, installations: installationDtos, lastCommits: includeActivity ? lastCommits : undefined }) + } catch (e) { + robot.log && robot.log.error && robot.log.error(e) + res.status(500).json({ error: e.message || 'unexpected error' }) + } + }) + + /** + * GET /api/safe-settings-hub/contents/* + * Fetches a file or directory listing from the SAFE_SETTINGS_HUB_ORG / SAFE_SETTINGS_HUB_REPO + * under the configured CONFIG_PATH (default .github). + * + * Examples: + * /api/safe-settings-hub/contents/ -> list CONFIG_PATH root + * /api/safe-settings-hub/contents/repos/foo.yml -> get specific file + * /api/safe-settings-hub/contents/repos?ref=main -> list directory at ref + * /api/safe-settings-hub/contents?recursive=true&maxDepth=2&fetchContent=false -> recursive listing without file bodies + * Note: recursive now defaults to true. Pass recursive=false for single-level listing. + */ + async function hubContent(req, res) { + try { + // Use cached installations (TTL-based freshness) + const installs = await cacheGetInstallations(robot) + const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === env.SAFE_SETTINGS_HUB_ORG.toLowerCase()) + if (!install) { + return res.status(404).json({ error: `Installation for org ${env.SAFE_SETTINGS_HUB_ORG} not found` }) + } + + const github = await robot.auth(install.id) + const wildcardPath = req.params[0] || '' // from the * in the route + const ref = req.query.ref || 'main' + const fullPath = wildcardPath ? path.posix.join(env.CONFIG_PATH, wildcardPath) : env.CONFIG_PATH + // recursive defaults to true unless explicitly disabled with recursive=false + const recursive = (req.query.recursive === 'false') ? false : true + let maxDepth = parseInt(req.query.maxDepth, 5) + if (isNaN(maxDepth) || maxDepth < 1) maxDepth = 5 // safety default + if (maxDepth > 8) maxDepth = 5 // hard cap to avoid abuse + // Unified flag: fetchContent (default true). No other legacy params supported. + const fetchContent = req.query.fetchContent === 'false' ? false : true + + // Commit metadata fetch with global shared cache + per-request memoization + const perRequestCommitCache = new Map() + const fetchCommitMeta = async (p) => { + if (perRequestCommitCache.has(p)) return perRequestCommitCache.get(p) + const cacheKey = `${ref}::${p}` + const cached = getCachedCommitMeta(cacheKey) + if (cached) { perRequestCommitCache.set(p, cached); return cached } + let meta + try { + const commits = await github.repos.listCommits({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, per_page: 1, path: p }) + .then(r => Array.isArray(r.data) ? r.data : []) + if (commits.length) { + const c = commits[0] + const committedAt = c.commit && c.commit.author && c.commit.author.date + const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null + meta = { + lastCommitSha: c.sha, + lastCommitAt: committedAt, + lastCommitMessage: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, + lastCommitAgeSeconds: ageSeconds + } + } else { + meta = { lastCommitSha: null, lastCommitAt: null, lastCommitMessage: null, lastCommitAgeSeconds: null } + } + } catch { + meta = { lastCommitSha: null, lastCommitAt: null, lastCommitMessage: null, lastCommitAgeSeconds: null } + } + setCachedCommitMeta(cacheKey, meta) + perRequestCommitCache.set(p, meta) + return meta + } + + // Helper to fetch a single file (returns null on failure) + const fetchFile = async (p) => { + try { + const fileResp = await github.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: p, ref }) + if (Array.isArray(fileResp.data)) return null + // file + const commitMeta = await fetchCommitMeta(fileResp.data.path) + if (fetchContent && typeof fileResp.data.content === 'string') { + const decoded = Buffer.from(fileResp.data.content, fileResp.data.encoding || 'base64').toString('utf8') + return { + type: fileResp.data.type, + name: path.posix.basename(p), + path: fileResp.data.path, + sha: fileResp.data.sha, + size: fileResp.data.size, + encoding: 'utf8', + content: decoded, + originalEncoding: fileResp.data.encoding || 'base64', + ...commitMeta + } + } + // metadata-only response + return { + type: fileResp.data.type, + name: path.posix.basename(p), + path: fileResp.data.path, + sha: fileResp.data.sha, + size: fileResp.data.size, + content: null, + originalEncoding: fileResp.data.encoding || 'base64', + ...commitMeta + } + } catch (e) { + robot.log && robot.log.warn && robot.log.warn(`Failed to fetch file ${p}: ${e.message}`) + return null + } + } + + // Recursive traversal with depth limiting and basic cycle protection + const seen = new Set() + // Concurrency limiter for directory entry processing + const MAX_DIR_CONCURRENCY = parseInt(process.env.DIR_ENTRY_CONCURRENCY || '6') + async function mapWithLimit(items, mapper) { + const out = [] + let i = 0 + const running = new Set() + async function run() { + if (i >= items.length) return + const idx = i++ + const p = Promise.resolve(mapper(items[idx], idx)).then(r => { out[idx] = r; running.delete(p) }) + running.add(p) + if (running.size >= MAX_DIR_CONCURRENCY) await Promise.race(running) + return run() + } + await run() + await Promise.all([...running]) + return out + } + + const traverseDir = async (dirPath, depth = 0) => { + if (depth >= maxDepth) { + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, depth, truncated: true, entries: [], ...commitMeta } + } + if (seen.has(dirPath)) { + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, depth, cycle: true, entries: [], ...commitMeta } + } + seen.add(dirPath) + let listing + try { + const resp = await github.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: dirPath, ref }) + if (!Array.isArray(resp.data)) { + // Not a directory; fetch as file instead + const f = await fetchFile(dirPath) + return f || { type: 'file', path: dirPath, error: 'unreadable' } + } + listing = resp.data + } catch (e) { + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, error: e.status === 404 ? 'not_found' : e.message, entries: [], ...commitMeta } + } + + const entries = await mapWithLimit(listing, async (item) => { + if (item.type === 'file') { + if (fetchContent) { + const f = await fetchFile(item.path) + if (f) return f + const commitMeta = await fetchCommitMeta(item.path) + return { type: 'file', name: item.name, path: item.path, sha: item.sha, size: item.size, content: null, ...commitMeta } + } + const commitMeta = await fetchCommitMeta(item.path) + return { type: 'file', name: item.name, path: item.path, sha: item.sha, size: item.size, content: null, ...commitMeta } + } else if (item.type === 'dir') { + return traverseDir(item.path, depth + 1) + } + const commitMeta = await fetchCommitMeta(item.path) + return { type: item.type, name: item.name, path: item.path, unsupported: true, ...commitMeta } + }) + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, depth, entries, ...commitMeta } + } + + const response = await github.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fullPath, + ref + }) + + const data = response.data + if (Array.isArray(data)) { + if (recursive) { + const tree = await traverseDir(fullPath, 0) + return res.json({ + recursive: true, + maxDepth, + ref: ref, + fetchContent, + ...tree + }) + } else { + // non-recursive (original behavior) + const entries = await Promise.all(data.map(async d => { + if (d.type === 'file') { + if (fetchContent) { + const f = await fetchFile(d.path) + if (f) return f + } + return { + name: d.name, + path: d.path, + type: d.type, + sha: d.sha, + size: d.size, + content: null + } + } + return { + name: d.name, + path: d.path, + type: d.type, + sha: d.sha, + size: d.size, + content: null + } + })) + return res.json({ + type: 'dir', + path: fullPath, + entries, + ref: ref, + fetchContent + }) + } + } + + if (typeof data.content === 'string') { + if (fetchContent) { + const decoded = Buffer.from(data.content, data.encoding || 'base64').toString('utf8') + return res.json({ + type: data.type, + path: data.path, + sha: data.sha, + size: data.size, + encoding: 'utf8', + content: decoded, + originalEncoding: data.encoding || 'base64', + ref, + fetchContent: true + }) + } + return res.json({ + type: data.type, + path: data.path, + sha: data.sha, + size: data.size, + content: null, + ref, + fetchContent: false + }) + } + // Unsupported type (symlink, submodule, etc.) + return res.status(415).json({ error: 'Unsupported content type returned by GitHub API' }) + } catch (e) { + if (e.status === 404) { + return res.status(404).json({ error: 'Not found' }) + } + robot.log && robot.log.error && robot.log.error(e) + return res.status(500).json({ error: e.message || 'unexpected error' }) + } + } + + router.get('/api/safe-settings-hub/content', hubContent) + router.get('/api/safe-settings-hub/content/*', hubContent) + + /** + * GET /api/settings/env + * Returns key/value pairs parsed from the project .env file excluding + * standard GitHub App infrastructure variables. + * Query params: + * includeInfra=true -> include normally excluded infrastructure vars + */ + router.get('/api/settings/env', (req, res) => { + try { + // Pull from the runtime env module (already merges defaults + process.env) + const exclude = new Set([ + 'APP_ID', 'WEBHOOK_SECRET', 'PRIVATE_KEY_PATH', 'WEBHOOK_PROXY_URL', 'LOG_LEVEL', + 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', 'PRIVATE_KEY', 'NODE_ENV' + ]) + const includeInfra = req.query.includeInfra === 'true' + // env object contains only the app's known config keys; supplement with a few additional custom vars from process.env if needed + const baseEntries = Object.entries(env) + const extraKeys = ['ENABLE_PR_COMMENT', 'SAFE_SETTINGS_HUB_REPO', 'SAFE_SETTINGS_HUB_ORG'] + extraKeys.forEach(k => { + if (!(k in env) && process.env[k] !== undefined) baseEntries.push([k, process.env[k]]) + }) + const variables = baseEntries + .filter(([k]) => includeInfra || !exclude.has(k)) + .map(([key, value]) => ({ key, value })) + .sort((a, b) => a.key.localeCompare(b.key)) + return res.json({ updatedAt: new Date().toISOString(), count: variables.length, variables }) + } catch (e) { + robot.log && robot.log.error && robot.log.error(e) + return res.status(500).json({ error: e.message || 'unexpected error' }) + } + }) + + // Cache metadata endpoint + router.get('/api/meta/installations', async (req, res) => { + try { + const installs = await cacheGetInstallations(robot) + const orgs = getOrgLogins() + const last = getLastFetchedAt() + return res.json({ + installations: installs.length, + organizations: orgs.length, + lastFetchedAt: last ? last.toISOString() : null, + ttlMs: process.env.INSTALLATION_CACHE_TTL_MS || '60000' + }) + } catch (e) { + robot.log && robot.log.error && robot.log.error(e) + return res.status(500).json({ error: e.message }) + } + }) + + return router +} + +module.exports = { setupRoutes } diff --git a/package-lock.json b/package-lock.json index 92cf50ebd..cd1411e61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^12.0.2", + "@octokit/auth-app": "^8.0.2", "@probot/adapter-aws-lambda-serverless": "^4.0.3", "deepmerge": "^4.3.1", "eta": "^3.5.0", @@ -1753,68 +1754,6 @@ "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.1.tgz", - "integrity": "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "toad-cache": "^3.7.0", - "universal-github-app-jwt": "^2.2.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-app": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", - "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-device": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", - "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", - "dependencies": { - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-user": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", - "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/@octokit/app/node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -1877,28 +1816,6 @@ "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/oauth-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", - "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", @@ -1978,156 +1895,325 @@ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" }, - "node_modules/@octokit/app/node_modules/universal-github-app-jwt": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", - "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==" - }, "node_modules/@octokit/app/node_modules/universal-user-agent": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" }, "node_modules/@octokit/auth-app": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.3.tgz", - "integrity": "sha512-dcaiteA6Y/beAlDLZOPNReN3FGHu+pARD6OHfh3T9f3EO09++ec+5wt3KtGGSSs2Mp5tI8fQwdMOEnrzBLfgUA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.2.tgz", + "integrity": "sha512-dLTmmA9gUlqiAJZgozfOsZFfpN/OldH3xweb7lqSnngax5Rs+PfO5dDlokaBfc41H1xOtsLYV5QqR0DkBAtPmw==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-app": "^7.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.1.0", - "deprecation": "^2.3.1", - "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", - "universal-github-app-jwt": "^1.1.2", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-app": "^9.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, - "node_modules/@octokit/auth-app/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "node_modules/@octokit/auth-app/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, - "node_modules/@octokit/auth-app/node_modules/lru-cache": { - "name": "@wolfy1339/lru-cache", - "version": "11.0.2-patch.1", - "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", - "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", - "license": "ISC", + "node_modules/@octokit/auth-app/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, "engines": { - "node": "18 >=18.20 || 20 || >=22" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-app/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/@octokit/auth-oauth-app": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", - "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", + "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "@types/btoa-lite": "^1.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-oauth-app/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/@octokit/auth-oauth-device": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", - "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", + "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", "license": "MIT", "dependencies": { - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/oauth-methods": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-oauth-device/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/@octokit/auth-oauth-user": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", - "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", + "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/oauth-methods": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-oauth-user/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -2223,69 +2309,25 @@ "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, - "node_modules/@octokit/oauth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz", - "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/auth-unauthenticated": "^7.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/oauth-methods": "^6.0.0", - "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-app": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", - "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-device": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", - "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "dependencies": { - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" + "@octokit/openapi-types": "^22.2.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-user": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", - "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", + "node_modules/@octokit/oauth-app": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz", + "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/auth-oauth-app": "^9.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/auth-unauthenticated": "^7.0.1", + "@octokit/core": "^7.0.2", + "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@types/aws-lambda": "^8.10.83", "universal-user-agent": "^7.0.0" }, "engines": { @@ -2354,28 +2396,6 @@ "node": ">= 20" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", - "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/@octokit/oauth-app/node_modules/@octokit/openapi-types": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", @@ -2426,45 +2446,91 @@ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" }, "node_modules/@octokit/oauth-authorization-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", - "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", + "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/oauth-methods": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", - "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", + "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", "license": "MIT", "dependencies": { - "@octokit/oauth-authorization-url": "^6.0.2", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0" + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/oauth-methods/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/@octokit/openapi-types": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", @@ -3675,9 +3741,9 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", - "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { "@types/ms": "*", @@ -9678,12 +9744,12 @@ } }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -10586,6 +10652,172 @@ "@octokit/core": ">=5" } }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-app": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.4.tgz", + "integrity": "sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/octokit-auth-probot/node_modules/lru-cache": { + "name": "@wolfy1339/lru-cache", + "version": "11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", + "license": "ISC", + "engines": { + "node": "18 >=18.20 || 20 || >=22" + } + }, + "node_modules/octokit-auth-probot/node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, "node_modules/octokit/node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -13219,14 +13451,10 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universal-github-app-jwt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", - "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", - "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "^9.0.0", - "jsonwebtoken": "^9.0.2" - } + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" }, "node_modules/universal-user-agent": { "version": "6.0.1", diff --git a/package.json b/package.json index e765fb80e..d295fce1a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^12.0.2", + "@octokit/auth-app": "^8.0.2", "@probot/adapter-aws-lambda-serverless": "^4.0.3", "deepmerge": "^4.3.1", "eta": "^3.5.0", diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json new file mode 100644 index 000000000..97a2bb84e --- /dev/null +++ b/ui/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next", "next/core-web-vitals"] +} diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..26b002aac --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for commiting if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..4f3cc28d3 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,40 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/route.ts`. The page auto-updates as you edit the file. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + +## API Routes + +This directory contains example API routes for the headless API app. + +For more details, see [route.js file convention](https://nextjs.org/docs/app/api-reference/file-conventions/route). diff --git a/ui/favicon.ico b/ui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3a78f556b0930171aacf738ccee96a5fc446928b GIT binary patch literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu literal 0 HcmV?d00001 diff --git a/ui/next.config.js b/ui/next.config.js new file mode 100644 index 000000000..d5d2d2c15 --- /dev/null +++ b/ui/next.config.js @@ -0,0 +1,10 @@ + +const nextConfig = { + output: "export", + // Disable Next.js ESLint checks during builds + eslint: { + ignoreDuringBuilds: true, + }, +}; + +module.exports = nextConfig; diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 000000000..8ab1771e6 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,5059 @@ +{ + "name": "ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.1.0", + "dependencies": { + "@primer/octicons-react": "^19.15.5", + "bootstrap": "^5.3.7", + "next": "15.4.7" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.4.7", + "tailwindcss": "^4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.7.tgz", + "integrity": "sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.7.tgz", + "integrity": "sha512-asj3RRiEruRLVr+k2ZC4hll9/XBzegMpFMr8IIRpNUYypG86m/a76339X2WETl1C53A512w2INOc2KZV769KPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.7.tgz", + "integrity": "sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.7.tgz", + "integrity": "sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.7.tgz", + "integrity": "sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.7.tgz", + "integrity": "sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.7.tgz", + "integrity": "sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.7.tgz", + "integrity": "sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.7.tgz", + "integrity": "sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.7.tgz", + "integrity": "sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@primer/octicons-react": { + "version": "19.15.5", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-19.15.5.tgz", + "integrity": "sha512-JEoxBVkd6F8MaKEO1QKau0Nnk3IVroYn7uXGgMqZawcLQmLljfzua3S1fs2FQs295SYM9I6DlkESgz5ORq5yHA==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz", + "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "postcss": "^8.4.41", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.40.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/bootstrap": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", + "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/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, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.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==", + "devOptional": true, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.7.tgz", + "integrity": "sha512-tkKKNVJKI4zMIgTpvG2x6mmdhuOdgXUL3AaSPHwxLQkvzi4Yryqvk6B0R5Z4gkpe7FKopz3ZmlpePH3NTHy3gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.4.7", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "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.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.4.7.tgz", + "integrity": "sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==", + "license": "MIT", + "dependencies": { + "@next/env": "15.4.7", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.4.7", + "@next/swc-darwin-x64": "15.4.7", + "@next/swc-linux-arm64-gnu": "15.4.7", + "@next/swc-linux-arm64-musl": "15.4.7", + "@next/swc-linux-x64-gnu": "15.4.7", + "@next/swc-linux-x64-musl": "15.4.7", + "@next/swc-win32-arm64-msvc": "15.4.7", + "@next/swc-win32-x64-msvc": "15.4.7", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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, + "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/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/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, + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..bc0097754 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,23 @@ +{ + "name": "ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "export": "next export", + "start": "next start" + }, + "dependencies": { + "@primer/octicons-react": "^19.15.5", + "bootstrap": "^5.3.7", + "next": "15.4.7" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.4.7", + "tailwindcss": "^4" + } +} diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 000000000..7762395be --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/public/shield.png b/ui/public/shield.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e4b6ec7bcbde72dcd715b6269b22e48c668976 GIT binary patch literal 24289 zcmXtgV|-oT^Yy*4bz?TRjmEaq*iIVTjcu!8o1|f5+qTWdcJkcs@AH4&oO9mY*?acP znl)?ojZ{{YLPo$x0002UGScFz000md{9OSD1Acb=3f=?1fG(<1qJXMN!V>`C3qVF( zMBUTitOMSYP%BmFrLC#Fbs>qN`uh7&Em1Y8nG^Px=)dxTc7K3jc#U2TxEY_p#@}EL z{kD8GH+|SiLr4jrdhv0krIA8-;7M7`$Y1|4Y(6R^oqMWy$o;!0_qiP7Z7N?#YEW-7 z(~YcnJ4t3(=xF*kUul?s?*WxWo+vC2uXnR>itj8tff5Bxt$Vy)fAMQ;bbv4`$2~?? zn6T(EcimkczP;=Dg*9%C1E;F38+Uxe=W2U*!{@OTx@1Y3miy`abuos%vD)YUV-2-G zhmH3fH%S)NY1Ykep)oL64Fp?i1QXgowe13%daXK9pV`eIIWX-l-Ld0SW-Mg#`-gcxm_?|I^CNGUY_Kli@*MoLIa_CgL+nL?n+PGp^sh$x3oxUPY%!7d4Q

xNbM`PG=BC2pI)-gc#?ujEY|nz)%svQ;+C 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 M07*qoM6N<$g22I`vj6}9 literal 0 HcmV?d00001 diff --git a/ui/shield.png b/ui/shield.png new file mode 100644 index 0000000000000000000000000000000000000000..3a78f556b0930171aacf738ccee96a5fc446928b GIT binary patch literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu literal 0 HcmV?d00001 diff --git a/ui/src/app/[slug]/route.js b/ui/src/app/[slug]/route.js new file mode 100644 index 000000000..290f31842 --- /dev/null +++ b/ui/src/app/[slug]/route.js @@ -0,0 +1,18 @@ +const { NextResponse } = require('next/server'); + +// JS version: no type annotations +export async function GET(request, context) { + const params = context && context.params ? context.params : {}; + const slug = params.slug || ''; + return NextResponse.json({ message: `Hello ${slug}!` }); +} + +export async function generateStaticParams() { + // Replace with your actual slugs + return [ + { slug: 'example1' }, + { slug: 'example2' } + ]; +} + +export const dynamic = 'force-static'; \ No newline at end of file diff --git a/ui/src/app/components/EnvVariables.jsx b/ui/src/app/components/EnvVariables.jsx new file mode 100644 index 000000000..9773cc064 --- /dev/null +++ b/ui/src/app/components/EnvVariables.jsx @@ -0,0 +1,161 @@ +'use client'; +import React, { useEffect, useState, useMemo } from 'react'; +import { SearchIcon, SyncIcon, EyeClosedIcon, EyeIcon, ShieldIcon, CopyIcon, ChevronUpIcon, ChevronDownIcon } from '@primer/octicons-react'; +import { useHydrated } from '../hooks/useHydrated'; + +const SENSITIVE_REGEX = /(secret|token|key|password|private)/i; + +export default function EnvVariables() { + const hydrated = useHydrated(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [rows, setRows] = useState([]); + const [search, setSearch] = useState(''); + const [includeInfra, setIncludeInfra] = useState(false); + const [revealAll, setRevealAll] = useState(false); + const [lastFetchedAt, setLastFetchedAt] = useState(null); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); + + const fetchData = () => { + if (!hydrated) return; + setLoading(true); setError(null); + fetch(`/api/settings/env${includeInfra ? '?includeInfra=true' : ''}`) + .then(r => { + if (!r.ok) { + throw new Error(`Unable to retrieve environment variables (HTTP ${r.status}). Please try again later.`); + } + return r.json(); + }) + .then(json => { + setRows(json.variables || []); + setLastFetchedAt(new Date(json.updatedAt || Date.now())); + }) + .catch(e => setError(e.message)) + .finally(() => setLoading(false)); + }; + + useEffect(() => { fetchData(); /* eslint-disable-next-line */ }, [hydrated, includeInfra]); + + const filtered = useMemo(() => { + if (!search) return rows; + const q = search.toLowerCase(); + return rows.filter(r => r.key.toLowerCase().includes(q) || (r.value + '').toLowerCase().includes(q)); + }, [rows, search]); + + const sorted = useMemo(() => { + if (!sortConfig.key || !sortConfig.direction) return filtered; + const list = [...filtered]; + list.sort((a, b) => { + let av = a[sortConfig.key]; + let bv = b[sortConfig.key]; + if (av == null) av = ''; + if (bv == null) bv = ''; + av = (av + '').toLowerCase(); + bv = (bv + '').toLowerCase(); + if (av < bv) return sortConfig.direction === 'asc' ? -1 : 1; + if (av > bv) return sortConfig.direction === 'asc' ? 1 : -1; + return 0; + }); + return list; + }, [filtered, sortConfig]); + + const cycleSort = (key) => { + setSortConfig(prev => { + if (prev.key === key) { + if (prev.direction === 'asc') return { key, direction: 'desc' }; + if (prev.direction === 'desc') return { key: null, direction: null }; + } + return { key, direction: 'asc' }; + }); + }; + + const renderSortIcon = (key) => { + if (sortConfig.key !== key) return ; + if (sortConfig.direction === 'asc') return ; + if (sortConfig.direction === 'desc') return ; + return ; + }; + + const maskedValue = (k, v) => { + if (revealAll) return v; + if (!SENSITIVE_REGEX.test(k)) return v; + if (!v) return v; + if (v.length <= 4) return '*'.repeat(v.length); + return v.slice(0, 2) + '***' + v.slice(-2); + }; + + const copyToClipboard = (text) => { + try { navigator.clipboard.writeText(text); } catch(_) {} + } + + return ( +

+
+
+ +
+ + setSearch(e.target.value)} /> +
+
+
+ +
+ setIncludeInfra(e.target.checked)} /> + +
+
+ setRevealAll(e.target.checked)} /> + +
+
+
+
+ + +
+
+
+ + {loading &&
Loading…
} + {error && !loading &&
Error: {error}
} + {!loading && !error && filtered.length === 0 &&
No variables
} + + {!loading && !error && filtered.length > 0 && ( +
+ + + + + + + + + + + {sorted.map(r => { + const sensitive = SENSITIVE_REGEX.test(r.key); + return ( + + + + + + + ); + })} + +
cycleSort('key')} className="theme-text-primary user-select-none" style={{ width: '28%', cursor: 'pointer' }}>Key {renderSortIcon('key')} cycleSort('value')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Value {renderSortIcon('value')}
{r.key} + {maskedValue(r.key, r.value)} + {sensitive && } + +
+
+ )} +
+ {sorted.length} shown / {rows.length} total + {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} +
+
+ ); +} diff --git a/ui/src/app/components/OrganizationsTable.jsx b/ui/src/app/components/OrganizationsTable.jsx new file mode 100644 index 000000000..56bd12c41 --- /dev/null +++ b/ui/src/app/components/OrganizationsTable.jsx @@ -0,0 +1,252 @@ +'use client'; + +import React, { useState, useMemo, useEffect } from 'react'; +import { ChevronUpIcon, ChevronDownIcon, SearchIcon } from '@primer/octicons-react'; +import { useHydrated } from '../hooks/useHydrated'; + +// Mock organizations used when /api/organizations returns 404 +const MOCK_ORGS = [ + { id: 1, name: 'mock-org-one', lastSyncDate: new Date(Date.now() - 3600 * 1000).toISOString(), lastSyncMessage: 'Initial mock sync', lastSyncSha: 'abcdef1', ageSeconds: 3600 }, + { id: 2, name: 'example-inc', lastSyncDate: new Date(Date.now() - 7200 * 1000).toISOString(), lastSyncMessage: 'Second mock sync', lastSyncSha: 'abcdef2', ageSeconds: 7200 }, + { id: 3, name: 'demo-labs', lastSyncDate: null, lastSyncMessage: null, lastSyncSha: null, ageSeconds: null, na: true } +]; + +const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [fetched, setFetched] = useState([]); + const hydrated = useHydrated(); + + // Fetch real organizations from backend API on client hydration + useEffect(() => { + if (!hydrated) return; // avoid SSR mismatch + let cancelled = false; + setLoading(true); + fetch('/api/organizations') + .then(r => { + if (!r.ok) { + throw new Error(`Unable to retrieve organizations (HTTP ${r.status}). Please try again later.`); + } + return r.json(); + }) + .then(json => { + if (!json || cancelled) return; + const lastCommits = json.lastCommits || {} + const mapped = (json.installations || []).map(i => { + const lc = lastCommits[i.account]; + return { + id: i.id, + name: i.account, + lastSyncDate: lc && lc.committed_at ? lc.committed_at : null, + lastSyncSha: lc && lc.sha ? lc.sha : null, + lastSyncMessage: lc && lc.message ? lc.message : null, + ageSeconds: lc && typeof lc.age_seconds === 'number' ? lc.age_seconds : null, + na: lc && lc.na === true + }; + }); + setFetched(mapped); + setError(null); + }) + .catch(e => { if (!cancelled) setError(e.message); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [hydrated]); + + const data = fetched.length > 0 ? fetched : (propOrganizations.length > 0 ? propOrganizations : []); + + // Format date for display with hydration-safe approach + const formatLastSync = (org) => { + if (org.na) return NA; + if (!org.lastSyncDate) return ; + const dateObj = new Date(org.lastSyncDate); + let ageSec = org.ageSeconds; + if (hydrated && (ageSec == null)) { + ageSec = Math.floor((Date.now() - dateObj.getTime()) / 1000); + } + const rel = (() => { + if (ageSec == null) return ''; + if (ageSec < 60) return '0m'; + const mTotal = Math.floor(ageSec / 60); + if (mTotal < 60) return `${mTotal}m`; + const hTotal = Math.floor(mTotal / 60); + if (hTotal < 24) { + const remM = mTotal % 60; + return remM ? `${hTotal}h ${remM}m` : `${hTotal}h`; + } + const dTotal = Math.floor(hTotal / 24); + const remH = hTotal % 24; + return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; + })(); + const fullStamp = `${dateObj.getFullYear()}-${String(dateObj.getMonth()+1).padStart(2,'0')}-${String(dateObj.getDate()).padStart(2,'0')} ${String(dateObj.getHours()).padStart(2,'0')}:${String(dateObj.getMinutes()).padStart(2,'0')}:${String(dateObj.getSeconds()).padStart(2,'0')}`; + const tooltip = [fullStamp, org.lastSyncMessage, org.lastSyncSha ? `SHA: ${org.lastSyncSha.slice(0,7)}` : null] + .filter(Boolean) + .join('\n'); + return {rel}; + }; + const lastSyncColStyle = { width: '170px', fontVariantNumeric: 'tabular-nums' }; + + // Filter organizations based on search term + const filteredData = useMemo(() => { + return data.filter(org => + org.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [data, searchTerm]); + + // Sort organizations + const sortedData = useMemo(() => { + if (!sortConfig.key) return filteredData; + + return [...filteredData].sort((a, b) => { + let aValue = a[sortConfig.key]; + let bValue = b[sortConfig.key]; + + // Convert dates to timestamps for comparison + if (sortConfig.key === 'lastSyncDate') { + aValue = new Date(aValue).getTime(); + bValue = new Date(bValue).getTime(); + } + + if (aValue < bValue) { + return sortConfig.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === 'asc' ? 1 : -1; + } + return 0; + }); + }, [filteredData, sortConfig]); + + // Handle column sorting + const handleSort = (key) => { + setSortConfig(prevConfig => { + if (prevConfig.key === key) { + if (prevConfig.direction === 'asc') { + return { key, direction: 'desc' }; + } else if (prevConfig.direction === 'desc') { + return { key: null, direction: null }; + } + } + return { key, direction: 'asc' }; + }); + }; + + // Render sort icon + const renderSortIcon = (columnKey) => { + if (sortConfig.key !== columnKey) { + return ; + } + if (sortConfig.direction === 'asc') { + return ; + } + if (sortConfig.direction === 'desc') { + return ; + } + return ; + }; + + return ( +
+ {/* Search Bar */} +
+
+
+
+ + + + setSearchTerm(e.target.value)} + /> +
+
+
+ + Showing {sortedData.length} of {data.length} organizations + +
+
+
+ + {/* Table */} +
+ + + + + + + + + {loading && ( + + + + )} + {!loading && error && ( + + + + )} + {!loading && !error && sortedData.length > 0 ? ( + sortedData.map((org) => ( + + + + + )) + ) : ( + !loading && !error && ( + + + + ) + )} + +
handleSort('name')} + > + Organization Name + {renderSortIcon('name')} + handleSort('lastSyncDate')} + > + Last Safe-settings Sync + {renderSortIcon('lastSyncDate')} +
Loading organizations…
Error: {error}
+ {org.name} + + {formatLastSync(org)} +
+ {searchTerm ? `No organizations found matching "${searchTerm}"` : 'No organizations available'} +
+
+ + {/* Table Footer Info */} + {sortedData.length > 0 && ( +
+ + {searchTerm && `Filtered by: "${searchTerm}"`} + {sortConfig.key && ( + + • Sorted by: {sortConfig.key === 'name' ? 'Organization Name' : 'Last Safe-settings Sync'} + ({sortConfig.direction === 'asc' ? 'A-Z' : 'Z-A'}) + + )} + +
+ )} +
+ ); +}; + +export default OrganizationsTable; diff --git a/ui/src/app/components/Safe-settings-hubContent.jsx b/ui/src/app/components/Safe-settings-hubContent.jsx new file mode 100644 index 000000000..e4292aebc --- /dev/null +++ b/ui/src/app/components/Safe-settings-hubContent.jsx @@ -0,0 +1,412 @@ +'use client'; +import React, { useEffect, useState, useMemo, useCallback } from 'react'; +import { SearchIcon, SyncIcon, FileIcon, FileDirectoryIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react'; +import { useHydrated } from '../hooks/useHydrated'; + +// Simple mock tree used when API returns 404 (dev convenience) +const MOCK_TREE = { + name: '.github', + path: '.github', + type: 'dir', + lastCommitAt: new Date(Date.now() - 3600 * 1000).toISOString(), + entries: [ + { + name: 'settings.yml', + path: '.github/settings.yml', + type: 'file', + lastCommitAt: new Date(Date.now() - 1800 * 1000).toISOString(), + lastCommitMessage: 'chore: mock settings', + lastCommitSha: 'mock123' + }, + { + name: 'CODEOWNERS', + path: '.github/CODEOWNERS', + type: 'file', + lastCommitAt: new Date(Date.now() - 7200 * 1000).toISOString(), + lastCommitMessage: 'feat: add mock CODEOWNERS', + lastCommitSha: 'mock456' + }, + { + name: 'workflows', + path: '.github/workflows', + type: 'dir', + lastCommitAt: new Date(Date.now() - 5400 * 1000).toISOString(), + entries: [ + { + name: 'ci.yml', + path: '.github/workflows/ci.yml', + type: 'file', + lastCommitAt: new Date(Date.now() - 2500 * 1000).toISOString(), + lastCommitMessage: 'ci: mock workflow', + lastCommitSha: 'mock789' + } + ] + } + ] +}; + +export default function SafeSettingsHubContent() { + const hydrated = useHydrated(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [rootTree, setRootTree] = useState(null); // recursive tree response + const [search, setSearch] = useState(''); + // Tree view removed; we now render a flattened table. + const [lastFetchedAt, setLastFetchedAt] = useState(null); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); // direction: 'asc' | 'desc' | null + const [expandedPaths, setExpandedPaths] = useState(() => new Set()); // which directory paths are expanded + + const fetchData = () => { + if (!hydrated) return; + setLoading(true); setError(null); + // Always ask for recursive tree; server may limit depth + // Explicitly request content bodies (fetchContent=true is default but sent for clarity) + fetch('/api/safe-settings-hub/content?fetchContent=true') + .then(r => { + if (!r.ok) { + // Surface a clear error message instead of falling back to mock data + throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status}). Please try again later.`); + } + return r.json(); + }) + .then(json => { + // On success set the returned tree + setRootTree(json); + setLastFetchedAt(new Date()); + }) + .catch(e => setError(e.message)) + .finally(() => setLoading(false)); + }; + + useEffect(() => { fetchData(); }, [hydrated]); + + // Flatten nodes for table display + const flattenNodes = useCallback((node, acc = [], depth = 0) => { + if (!node) return acc; + acc.push({ + name: node.name, + path: node.path, + type: node.type, + lastCommitAt: node.lastCommitAt, + lastCommitMessage: node.lastCommitMessage, + lastCommitSha: node.lastCommitSha, + depth + }); + if (node.type === 'dir' && Array.isArray(node.entries)) { + node.entries.forEach(child => flattenNodes(child, acc, depth + 1)); + } + return acc; + }, []); + + const filterTree = useCallback((node) => { + if (!node) return null; + const term = search.toLowerCase(); + const matches = (n) => !term || n.name.toLowerCase().includes(term) || n.path.toLowerCase().includes(term); + if (node.type === 'file') { + return matches(node) ? node : null; + } + if (node.type === 'dir') { + const children = (node.entries || []).map(filterTree).filter(Boolean); + if (matches(node) || children.length > 0) { + return { ...node, entries: children }; + } + return null; + } + return null; + }, [search]); + + const filteredTree = useMemo(() => filterTree(rootTree), [rootTree, filterTree]); + + // If the root contains a top-level 'safe-settings' directory, treat that directory as the display root + const displayTree = useMemo(() => { + if (!filteredTree) return null; + if (filteredTree.type === 'dir') { + const nameMatch = (n) => n && n.type === 'dir' && n.name && n.name.toLowerCase().includes('safe-settings'); + // Prefer immediate child named 'safe-settings' + const immediate = (filteredTree.entries || []).find(nameMatch); + if (immediate) return immediate; + // Fallback: search descendants up to a small depth for 'safe-settings' + const findDescendant = (node, depth = 0, maxDepth = 3) => { + if (!node || node.type !== 'dir' || depth >= maxDepth) return null; + for (const child of node.entries || []) { + if (nameMatch(child)) return child; + } + for (const child of node.entries || []) { + if (child.type === 'dir') { + const found = findDescendant(child, depth + 1, maxDepth); + if (found) return found; + } + } + return null; + }; + const found = findDescendant(filteredTree, 0, 3); + if (found) return found; + } + return filteredTree; + }, [filteredTree]); + + // When a search filter is applied, auto-expand all ancestor directories that contain matches + useEffect(() => { + if (!search) return; // only on active filter + if (!displayTree || displayTree.type !== 'dir') return; + const dirsToExpand = new Set(); + const walk = (node) => { + if (!node || node.type !== 'dir') return false; + let containsMatch = false; + for (const child of node.entries || []) { + if (child.type === 'dir') { + if (walk(child)) { + containsMatch = true; + dirsToExpand.add(child.path); // expand child dir to show deeper matches + } + } else { + // Any file present means this dir should be opened if it passed filtering + containsMatch = true; + } + } + return containsMatch; + }; + walk(displayTree); + // Also expand top-level dirs that survived filtering and have entries + (displayTree.entries || []).forEach(e => { if (e.type === 'dir') dirsToExpand.add(e.path); }); + setExpandedPaths(prev => { + const next = new Set(prev); + dirsToExpand.forEach(p => next.add(p)); + return next; + }); + }, [search, displayTree]); + + const flatList = useMemo(() => { + if (!displayTree) return []; + // If display root is a directory, list its children instead of the directory itself (hide intermediate root) + if (displayTree.type === 'dir') { + return displayTree.entries.flatMap(child => flattenNodes(child, [], 0)); + } + return flattenNodes(displayTree, [], 0); + }, [displayTree, flattenNodes]); + + // Build hierarchical visible list honoring expandedPaths and optional sorting + const sortedFlatList = useMemo(() => { + if (!displayTree) return []; + // function to sort entries inside a directory when sorting enabled + const sortEntries = (entries) => { + if (!sortConfig.key || !sortConfig.direction) return entries; + const key = sortConfig.key; + return [...entries].sort((a, b) => { + let av; let bv; + switch (key) { + case 'name': av = a.name.toLowerCase(); bv = b.name.toLowerCase(); break; + case 'path': av = a.path.toLowerCase(); bv = b.path.toLowerCase(); break; + case 'lastCommitAt': av = a.lastCommitAt ? new Date(a.lastCommitAt).getTime() : 0; bv = b.lastCommitAt ? new Date(b.lastCommitAt).getTime() : 0; break; + default: av = a[key]; bv = b[key]; + } + if (av < bv) return sortConfig.direction === 'asc' ? -1 : 1; + if (av > bv) return sortConfig.direction === 'asc' ? 1 : -1; + return 0; + }); + }; + const out = []; + const process = (node, depth) => { + if (!node) return; + if (node.type === 'dir') { + const children = sortEntries(node.entries || []); + children.forEach(child => { + out.push({ + name: child.name, + path: child.path, + type: child.type, + lastCommitAt: child.lastCommitAt, + lastCommitMessage: child.lastCommitMessage, + lastCommitSha: child.lastCommitSha, + depth + }); + if (child.type === 'dir' && expandedPaths.has(child.path)) { + process(child, depth + 1); + } + }); + } else { + out.push({ + name: node.name, + path: node.path, + type: node.type, + lastCommitAt: node.lastCommitAt, + lastCommitMessage: node.lastCommitMessage, + lastCommitSha: node.lastCommitSha, + depth + }); + } + }; + // Start processing at displayTree (hiding any intermediate 'safe-settings' wrapper) + process(displayTree, 0); + return out; + }, [displayTree, sortConfig, expandedPaths]); + + const cycleSort = (key) => { + setSortConfig(prev => { + if (prev.key === key) { + if (prev.direction === 'asc') return { key, direction: 'desc' }; + if (prev.direction === 'desc') return { key: null, direction: null }; // clear + } + return { key, direction: 'asc' }; + }); + }; + + const renderSortIcon = (key) => { + if (sortConfig.key !== key) return ; + if (sortConfig.direction === 'asc') return ; + if (sortConfig.direction === 'desc') return ; + return ; + }; + + const toggleDir = (path) => { + setExpandedPaths(prev => { + const next = new Set(prev); + if (next.has(path)) next.delete(path); else next.add(path); + return next; + }); + }; + + const collectAllDirPaths = useCallback((node, acc = []) => { + if (!node) return acc; + if (node.type === 'dir') { + if (node.path && node.path !== '.github') acc.push(node.path); // skip synthetic root label + (node.entries || []).forEach(child => collectAllDirPaths(child, acc)); + } + return acc; + }, []); + + const expandAll = () => { + if (!filteredTree) return; + const all = collectAllDirPaths(filteredTree, []); + setExpandedPaths(new Set(all)); + }; + + const collapseAll = () => setExpandedPaths(new Set()); + + const formatRelative = (iso) => { + if (!iso) return null; + const dt = new Date(iso); + let diffSec = Math.floor((Date.now() - dt.getTime()) / 1000); + if (diffSec < 0) diffSec = 0; + if (diffSec < 60) return '0m'; + const mTotal = Math.floor(diffSec / 60); + if (mTotal < 60) return `${mTotal}m`; + const hTotal = Math.floor(mTotal / 60); + if (hTotal < 24) { + const remM = mTotal % 60; + return remM ? `${hTotal}h ${remM}m` : `${hTotal}h`; + } + const dTotal = Math.floor(hTotal / 24); + const remH = hTotal % 24; + return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; + }; + + // Table columns: Name (indented), Path, Type, Last update + + return ( + <> +
+
+
+
+ + + + setSearch(e.target.value)} /> +
+
+
+
+ + + +
+
+
+ + {/*
+
+
+
+ + + + setSearchTerm(e.target.value)} + /> +
+
+
+ + Showing {sortedData.length} of {data.length} organizations + +
+
+
*/} + + {loading &&
Loading…
} + {error && !loading &&
Error: {error}
} + {!loading && !error && !displayTree &&
No entries
} + + {!loading && !error && displayTree && ( +
+
+ + + + + + + + + + {sortedFlatList.map(node => { + const isDir = node.type === 'dir'; + const expanded = isDir && expandedPaths.has(node.path); + return ( + isDir && toggleDir(node.path)} style={isDir ? { cursor: 'pointer' } : undefined}> + + + + + ); + })} + +
cycleSort('name')} className="theme-text-primary user-select-none" style={{ width: '35%', cursor: 'pointer' }}>Name {renderSortIcon('name')} cycleSort('path')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Path {renderSortIcon('path')} cycleSort('lastCommitAt')} className="theme-text-primary user-select-none" style={{ width: '170px', cursor: 'pointer' }}>Last update {renderSortIcon('lastCommitAt')}
+ + {isDir ? ( + expanded ? : + ) : ( + + )} + {isDir && } + {node.name} + + {node.path} + {node.lastCommitAt ? formatRelative(node.lastCommitAt) : '—'} +
+
+
+ )} +
+ {!loading && !error && ( +
+ {sortedFlatList.length} items shown + {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} +
+ )} + {/* Removed inner bordered wrapper styles so only outer page container shows a border */} + + ); +} diff --git a/ui/src/app/components/ThemeContext.jsx b/ui/src/app/components/ThemeContext.jsx new file mode 100644 index 000000000..75470143d --- /dev/null +++ b/ui/src/app/components/ThemeContext.jsx @@ -0,0 +1,71 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect } from 'react'; + +const ThemeContext = createContext(); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +export const ThemeProvider = ({ children }) => { + // Always start with 'light' for SSR consistency + const [theme, setTheme] = useState('light'); + const [mounted, setMounted] = useState(false); + + // Only run on client side + useEffect(() => { + setMounted(true); + + // Migrate old theme key if it exists + const oldTheme = localStorage.getItem('safe-settings-theme'); + if (oldTheme && !localStorage.getItem('theme')) { + localStorage.setItem('theme', oldTheme); + localStorage.removeItem('safe-settings-theme'); + } + + // Get theme from localStorage + const savedTheme = localStorage.getItem('theme') || 'light'; + + // Set state (will trigger re-render with correct theme) + setTheme(savedTheme); + + // Apply to DOM immediately to prevent flash + document.documentElement.setAttribute('data-theme', savedTheme); + }, []); + + // Apply theme changes to DOM + useEffect(() => { + if (mounted) { + document.documentElement.setAttribute('data-theme', theme); + } + }, [theme, mounted]); + + const toggleTheme = () => { + const newTheme = theme === 'light' ? 'dark' : 'light'; + setTheme(newTheme); + localStorage.setItem('theme', newTheme); + }; + + const setSpecificTheme = (themeName) => { + setTheme(themeName); + localStorage.setItem('theme', themeName); + }; + + // Render children immediately - no hiding + return ( + + {children} + + ); +}; diff --git a/ui/src/app/components/ThemeToggle.jsx b/ui/src/app/components/ThemeToggle.jsx new file mode 100644 index 000000000..803c9d997 --- /dev/null +++ b/ui/src/app/components/ThemeToggle.jsx @@ -0,0 +1,17 @@ +'use client'; + +import { useTheme } from './ThemeContext'; + +export default function ThemeToggle() { + const { theme, toggleTheme, isDark } = useTheme(); + + return ( + + ); +} diff --git a/ui/src/app/components/TitleBar.css b/ui/src/app/components/TitleBar.css new file mode 100644 index 000000000..d10a7ea76 --- /dev/null +++ b/ui/src/app/components/TitleBar.css @@ -0,0 +1,174 @@ +/* TitleBar Component-Specific Styles */ + +/* Header styles */ +.title-header { + background: #333; + color: #fff; + min-height: 60px; /* Ensure consistent height */ +} + +/* Theme-specific header styles */ +[data-theme="light"] .title-header, +body.light-theme .title-header { + background: #333; + color: #fff; +} + +[data-theme="dark"] .title-header, +body.dark-theme .title-header { + background: #161b22; + color: #f0f6fc; +} + +/* Navigation bar - consistent height and styling */ +.title-nav { + min-height: 48px; /* Consistent nav height */ + border-bottom: 1px solid var(--border-color, #dee2e6) !important; + background: var(--bg-secondary, #f6f8fa); /* Default light theme background */ +} + +/* Data-theme selectors for immediate theme application */ +[data-theme="light"] .title-nav, +body.light-theme .title-nav { + background: #f6f8fa; + color: #24292f; + border-bottom: 1px solid #dee2e6 !important; +} + +[data-theme="dark"] .title-nav, +body.dark-theme .title-nav { + background: #22272e; + color: #f6f8fa; + border-bottom: 1px solid #666a6e !important; +} + +/* Theme toggle button */ +.theme-toggle-btn { + border: none !important; + background: transparent !important; + cursor: pointer !important; + padding: 2px !important; + border-radius: 6px !important; +} + +.theme-toggle-btn .theme-toggle-icon { + transition: color 0.15s; + color: lightgray; /* Default icon color */ +} + +/* Theme-specific toggle button styles */ +[data-theme="light"] .theme-toggle-btn .theme-toggle-icon, +body.light-theme .theme-toggle-btn .theme-toggle-icon { + color: #fff; +} + +[data-theme="dark"] .theme-toggle-btn .theme-toggle-icon, +body.dark-theme .theme-toggle-btn .theme-toggle-icon { + color: #f0f6fc; +} + +[data-theme="light"] .theme-toggle-btn:hover, +body.light-theme .theme-toggle-btn:hover { + background: rgba(255, 255, 255, 0.1) !important; +} + +[data-theme="dark"] .theme-toggle-btn:hover, +body.dark-theme .theme-toggle-btn:hover { + background: rgba(240, 246, 252, 0.1) !important; +} + +.theme-toggle-btn:hover .theme-toggle-icon, +.theme-toggle-btn:focus .theme-toggle-icon { + color: yellow; /* Hover icon color */ +} + +/* Navigation links */ +.nav-link-custom { + border: none; +} + +/* Light theme nav links */ +[data-theme="light"] .nav-link-custom, +body.light-theme .nav-link-custom { + color: #24292f !important; +} + +/* Dark theme nav links */ +[data-theme="dark"] .nav-link-custom, +body.dark-theme .nav-link-custom { + color: #f6f8fa; +} + +/* Navigation menu items */ +.nav-link.menu-hover { + border-radius: 5px !important; + margin: 10px 10px 10px 10px !important; + padding: 5px 10px !important; + transition: background 0.15s, color 0.15s; + border: 1px solid transparent !important; /* Invisible border to maintain box model */ +} + +.nav-link.menu-hover:hover { + background: var(--bg-accent) !important; + border-radius: 5px !important; + border: 1px solid transparent !important; /* Keep same border width */ +} + +/* Theme-specific hover colors */ +[data-theme="light"] .nav-link.menu-hover:hover, +body.light-theme .nav-link.menu-hover:hover { + background-color: #eaecef !important; +} + +[data-theme="dark"] .nav-link.menu-hover:hover, +body.dark-theme .nav-link.menu-hover:hover { + background-color: #30363d !important; +} + +/* Override Bootstrap's default nav-tabs border-radius */ +.nav-tabs .nav-link { + border-radius: 5px !important; + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs .nav-link:hover { + border-radius: 5px !important; + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs .nav-link:focus { + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs .nav-link:active { + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs { + border-top: none; + border-bottom: none; +} + +.menu-hover.active { + background: transparent; + border: none !important; +} + +/* Active menu indicator */ +.menu-active-indicator { + position: absolute; + left: 0; + right: 0; + bottom: -10px; + height: 2px; + background: rgb(253, 140, 115); /* Orange-red underline color */ + border-radius: 1px; +} diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx new file mode 100644 index 000000000..be57b934e --- /dev/null +++ b/ui/src/app/components/TitleBar.jsx @@ -0,0 +1,84 @@ +"use client"; +import { usePathname } from "next/navigation"; +import React from "react"; +import { GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; +import { useTheme } from './ThemeContext'; +import './TitleBar.css'; + +export default function TitleBar() { + const pathname = usePathname(); + const { isDark, toggleTheme } = useTheme(); + + // Always render the TitleBar structure to prevent layout shift + return ( + <> +
+
+ + + + + Safe-Settings Dashboard + + +
+
+ + + ); +} diff --git a/ui/src/app/dashboard/env/page.jsx b/ui/src/app/dashboard/env/page.jsx new file mode 100644 index 000000000..6022b0b96 --- /dev/null +++ b/ui/src/app/dashboard/env/page.jsx @@ -0,0 +1,23 @@ +import TitleBar from "../../components/TitleBar"; +import EnvVariables from "../../components/EnvVariables"; + +export default function EnvVarsPage() { + return ( +
+ +
+
+

App Environment Settings

+

+ These are the current settings used by the app. Some values are hidden or + masked for security. +

+
+
+
+ +
+
+
+ ); +} diff --git a/ui/src/app/dashboard/organizations/page.jsx b/ui/src/app/dashboard/organizations/page.jsx new file mode 100644 index 000000000..e5712bf0e --- /dev/null +++ b/ui/src/app/dashboard/organizations/page.jsx @@ -0,0 +1,24 @@ +import TitleBar from "../../components/TitleBar"; +import OrganizationsTable from "../../components/OrganizationsTable"; + +export default function OrganizationsPage() { + return ( +
+ +
+
+

+ Organizations +

+

+ List all the installations of the App and the last time Safe-settings configurations were synced. +

+
+ +
+ +
+
+
+ ); +} diff --git a/ui/src/app/dashboard/page.jsx b/ui/src/app/dashboard/page.jsx new file mode 100644 index 000000000..92cd1ac12 --- /dev/null +++ b/ui/src/app/dashboard/page.jsx @@ -0,0 +1,13 @@ +import TitleBar from "../components/TitleBar"; + +export default function DashboardPage() { + return ( +
+ +
+

Welcome to the Safe-Settings Hub Dashboard

+

Select a menu item above to get started.

+
+
+ ); +} diff --git a/ui/src/app/dashboard/safe-settings-hub/page.jsx b/ui/src/app/dashboard/safe-settings-hub/page.jsx new file mode 100644 index 000000000..a8bbe5810 --- /dev/null +++ b/ui/src/app/dashboard/safe-settings-hub/page.jsx @@ -0,0 +1,25 @@ +import TitleBar from "../../components/TitleBar"; +import MasterAdminContents from "../../components/Safe-settings-hubContent"; + +export default function SafeSettingsHubConfigPage() { + return ( +
+ +
+
+

+ Safe-Settings Hub Content +

+

+ Listing files maintained by the Safe-Settings Global configuration (all ORG's). + Files are retrieved from `/api/safe-settings-hub/content`. +

+
+
+
+ +
+
+
+ ); +} diff --git a/ui/src/app/dashboard/settings/page.jsx b/ui/src/app/dashboard/settings/page.jsx new file mode 100644 index 000000000..e63d76620 --- /dev/null +++ b/ui/src/app/dashboard/settings/page.jsx @@ -0,0 +1,13 @@ +import TitleBar from "../../components/TitleBar"; + +export default function SettingsPage() { + return ( +
+ +
+

Settings

+

Settings options will go here.

+
+
+ ); +} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css new file mode 100644 index 000000000..d4fb0de01 --- /dev/null +++ b/ui/src/app/globals.css @@ -0,0 +1,260 @@ +/* Global Theme Variables */ +/* Default theme variables (light theme as default) */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-accent: #eaecef; + --text-primary: #24292f; + --text-secondary: #6c757d; + --border-color: #dee2e6; +} + +/* Theme variables based on data-theme attribute */ +[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-accent: #eaecef; + --text-primary: #24292f; + --text-secondary: #6c757d; + --border-color: #dee2e6; +} + +[data-theme="dark"] { + --bg-primary: rgb(13,17,22); + --bg-secondary: #444444; + --bg-accent: #30363d; + --text-primary: #f0f6fc; + --text-secondary: #dddddd; + --border-color: #4d4d4d; +} + +/* Legacy support for body classes */ +body.light-theme { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-accent: #eaecef; + --text-primary: #24292f; + --text-secondary: #6c757d; + --border-color: #dee2e6; +} + +body.dark-theme { + --bg-primary: #161b22; + --bg-secondary: #444444; + --bg-accent: #30363d; + --text-primary: #f0f6fc; + --text-secondary: #e3e3e3; +} + +/* Global Theme Styles */ +/* Default body styling (light theme as default) */ +body { + background: var(--bg-primary, #fff) !important; + color: var(--text-primary, #24292f) !important; +} + +/* Theme-specific body styles using data-theme */ +[data-theme="light"] body, +body.light-theme { + background: #fff; + color: #24292f; +} + +[data-theme="dark"] body, +body.dark-theme { + background: rgb(45, 46, 47); + color: #f6f8fa; +} + +/* Global Main Element Theme */ +[data-theme="light"] main, +body.light-theme main { + background: #fff; + color: #24292f; +} + +[data-theme="dark"] main, +body.dark-theme main { + /* background: #161b22; */ + color: #f6f8fa; +} + +[data-theme="light"] .nav-link, +body.light-theme .nav-link { + color: #24292f; +} + +[data-theme="dark"] .nav-link, +body.dark-theme .nav-link { + color: #f6f8fa !important; +} + +/* title bar nav tabs */ +[data-theme="dark"] .nav-tabs, +body.dark-theme .nav-tabs { + background: #22272e; + border: none !important; +} + +[data-theme="light"] .nav-tabs, +body.light-theme .nav-tabs { + border: none !important; +} + +/* Apply theme variables to main element */ +main { + color: var(--text-primary) !important; + padding: 1rem; + border-radius: 12px; + margin-top: 1rem; +} + +/* Theme Utility Classes */ +.theme-bg-primary { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.theme-bg-secondary { + background-color: var(--bg-secondary); + color: var(--text-primary) !important; +} + +.theme-bg-accent { + background-color: var(--bg-accent); + color: var(--text-primary) !important; +} + +.theme-text-primary { + color: var(--text-primary) !important; +} + +.theme-text-secondary { + color: var(--text-secondary) !important; +} + +.theme-border { + border-color: var(--border-color) !important; /* override bootstrap .border */ +} + +.border.theme-border, .theme-border.border { + border-color: var(--border-color) !important; +} +/* Global Font Utility Classes */ +.dark-font { + color: #f6f8fa; +} + +.light-font { + color: #24292f; +} + +/* Organizations Table Styles */ +.ui-table .table { + background-color: var(--bg-primary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .table thead th { + background-color: var(--bg-secondary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; + font-weight: 600; +} + +.ui-table .table tbody td { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .table tbody tr:hover { + background-color: var(--bg-accent) !important; +} + +.ui-table .sortable-header:hover { + background-color: var(--bg-accent) !important; +} + +.ui-table .input-group-text { + background-color: var(--bg-secondary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .form-control { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .form-control:focus { + background-color: var(--bg-primary); + color: var(--text-primary) !important; + border-color: var(--border-color) !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.ui-table .form-control::placeholder { + color: var(--text-secondary) !important; +} + +.list-group-item { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.list-group-item:hover { + background-color: var(--bg-accent) !important; +} + +.text-muted { + color: var(--text-secondary) !important; +} + +span.text-muted { + color: var(--text-secondary) !important; +} + +code { + color: var(--text-primary) !important; +} + +element { + color: var(--text-primary) !important; + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; +} + +.input-group-text { + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; + color: var(--text-primary) !important; +} + +.table { + border-radius: 12px !important; + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +/* Env vars table dark mode override */ +[data-theme="dark"] .env-vars table, body.dark-theme .env-vars table { + background-color: var(--bg-primary) !important; +} +[data-theme="dark"] .env-vars thead th, body.dark-theme .env-vars thead th { + background-color: var(--bg-secondary) !important; +} + +th{ + font-weight: 600; + background-color: var(--bg-secondary) !important; +} + +tr td{ + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} diff --git a/ui/src/app/hooks/useClientSafe.js b/ui/src/app/hooks/useClientSafe.js new file mode 100644 index 000000000..fabb0a510 --- /dev/null +++ b/ui/src/app/hooks/useClientSafe.js @@ -0,0 +1,45 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +/** + * Custom hook to handle client-side mounting + * Helps prevent hydration mismatches by ensuring client-specific code + * only runs after the component has mounted on the client + */ +export const useIsClient = () => { + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + return isClient; +}; + +/** + * Custom hook for client-safe date formatting + * Returns a consistent format between server and client to prevent hydration issues + */ +export const useClientSafeDate = () => { + const isClient = useIsClient(); + + const formatDate = (dateString) => { + if (!isClient) { + // Server-side: return a simple format that matches potential client output + return new Date(dateString).toISOString().split('T')[0]; + } + + // Client-side: full formatting + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + return { formatDate, isClient }; +}; diff --git a/ui/src/app/hooks/useHydrated.js b/ui/src/app/hooks/useHydrated.js new file mode 100644 index 000000000..7ab514a7f --- /dev/null +++ b/ui/src/app/hooks/useHydrated.js @@ -0,0 +1,18 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +/** + * Hook that ensures consistent rendering between server and client + * Prevents hydration mismatches by showing a simple version first, + * then upgrading to the full version after hydration + */ +export const useHydrated = () => { + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + setHydrated(true); + }, []); + + return hydrated; +}; diff --git a/ui/src/app/layout.jsx b/ui/src/app/layout.jsx new file mode 100644 index 000000000..1104d2572 --- /dev/null +++ b/ui/src/app/layout.jsx @@ -0,0 +1,41 @@ +import './globals.css'; +import { ThemeProvider } from './components/ThemeContext'; + +// (Optional) Next.js App Router metadata API – safe to add +export const metadata = { + title: 'Safe Settings', + description: 'Safe Settings dashboard', + icons: { + icon: [ + { url: '/favicon.svg', type: 'image/svg+xml' }, + { url: '/favicon.ico', sizes: 'any' } + ], + apple: '/apple-touch-icon.png', + shortcut: '/favicon.ico' + } +}; + +export default function RootLayout({ children }) { + return ( + + + {/* Existing Bootstrap CSS */} + + {/* Favicon / icons */} + + + {/* Optional apple-touch-icon (provide file or remove link) */} + {/* */} + + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/not-found.jsx b/ui/src/app/not-found.jsx new file mode 100644 index 000000000..da79f865d --- /dev/null +++ b/ui/src/app/not-found.jsx @@ -0,0 +1,15 @@ +"use client"; +import TitleBar from "./components/TitleBar"; + +export default function NotFound() { + return ( +
+ +
+

404

+

Sorry, the page you are looking for does not exist.

+ Go to Dashboard +
+
+ ); +} diff --git a/ui/src/app/route.js b/ui/src/app/route.js new file mode 100644 index 000000000..af7296c8e --- /dev/null +++ b/ui/src/app/route.js @@ -0,0 +1,7 @@ +const { NextResponse } = require('next/server'); + +export async function GET() { + return NextResponse.json({ message: 'Hello world!' }); +} + +export const dynamic = 'force-static'; From 6457790edf6cf8e8e3b35ec2e15d041e9178afa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Sun, 31 Aug 2025 21:30:18 -0400 Subject: [PATCH 2/5] adjusting ui --- docs/hubSyncHandler/README.md | 22 + lib/utils.js | 0 ui/favico.ico | Bin 0 -> 3961 bytes ui/favico.png | Bin 0 -> 3961 bytes ui/favicon.ico | Bin 3872 -> 0 bytes ui/shield.png | Bin 3872 -> 3961 bytes ui/src/app/components/EnvVariables.jsx | 3 +- .../components/Safe-settings-hubContent.jsx | 518 +++++++----------- ui/src/app/components/TitleBar.jsx | 18 +- ui/src/app/globals.css | 33 +- 10 files changed, 248 insertions(+), 346 deletions(-) create mode 100644 lib/utils.js create mode 100644 ui/favico.ico create mode 100644 ui/favico.png delete mode 100644 ui/favicon.ico diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index 7ffd23ab3..4f4d06c80 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -32,3 +32,25 @@ Environment variables specific to the 'Sync-Feature' | `SAFE_SETTINGS_HUB_DIRECT_PUSH` | Use a PR or direct commit | false | +--- +--- + +## Hub Sync Scenarios + +1. Sync the `Hub Admin Repo` changes to a `Safe-Settings Admin Repo` in **the same ORG** as the Hub Admin Repo. + +2. Sync the `Hub Admin changes` to a `Safe-Settings Admin Repo` in **a different ORG**. + +3. _`'Global'`_ `Hub Admin Repo` updates. +Changes will `applied to all Organization` + + +```mermaid +flowchart LR +PR --> Hub +Hub --> ORG-A +Hub -..- ORG-B +Hub -..- ORG-C + + +``` \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/favico.ico b/ui/favico.ico new file mode 100644 index 0000000000000000000000000000000000000000..bf67019abb1784abbeba87886cf3e5769b53534c GIT binary patch literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ literal 0 HcmV?d00001 diff --git a/ui/favico.png b/ui/favico.png new file mode 100644 index 0000000000000000000000000000000000000000..bf67019abb1784abbeba87886cf3e5769b53534c GIT binary patch literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ literal 0 HcmV?d00001 diff --git a/ui/favicon.ico b/ui/favicon.ico deleted file mode 100644 index 3a78f556b0930171aacf738ccee96a5fc446928b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu diff --git a/ui/shield.png b/ui/shield.png index 3a78f556b0930171aacf738ccee96a5fc446928b..bf67019abb1784abbeba87886cf3e5769b53534c 100644 GIT binary patch literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu diff --git a/ui/src/app/components/EnvVariables.jsx b/ui/src/app/components/EnvVariables.jsx index 9773cc064..fd7eedd15 100644 --- a/ui/src/app/components/EnvVariables.jsx +++ b/ui/src/app/components/EnvVariables.jsx @@ -153,8 +153,7 @@ export default function EnvVariables() { )}
- {sorted.length} shown / {rows.length} total - {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} + {sorted.length} shown / {rows.length} total
); diff --git a/ui/src/app/components/Safe-settings-hubContent.jsx b/ui/src/app/components/Safe-settings-hubContent.jsx index e4292aebc..3a40154c2 100644 --- a/ui/src/app/components/Safe-settings-hubContent.jsx +++ b/ui/src/app/components/Safe-settings-hubContent.jsx @@ -1,115 +1,57 @@ 'use client'; import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import { SearchIcon, SyncIcon, FileIcon, FileDirectoryIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react'; +import { SearchIcon, FileIcon, FileDirectoryIcon, ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react'; import { useHydrated } from '../hooks/useHydrated'; -// Simple mock tree used when API returns 404 (dev convenience) +// Match the left index width and reuse for the search input +const LEFT_COL_WIDTH = 320; + const MOCK_TREE = { name: '.github', path: '.github', type: 'dir', - lastCommitAt: new Date(Date.now() - 3600 * 1000).toISOString(), + lastCommitAt: new Date().toISOString(), entries: [ - { - name: 'settings.yml', - path: '.github/settings.yml', - type: 'file', - lastCommitAt: new Date(Date.now() - 1800 * 1000).toISOString(), - lastCommitMessage: 'chore: mock settings', - lastCommitSha: 'mock123' - }, - { - name: 'CODEOWNERS', - path: '.github/CODEOWNERS', - type: 'file', - lastCommitAt: new Date(Date.now() - 7200 * 1000).toISOString(), - lastCommitMessage: 'feat: add mock CODEOWNERS', - lastCommitSha: 'mock456' - }, - { - name: 'workflows', - path: '.github/workflows', - type: 'dir', - lastCommitAt: new Date(Date.now() - 5400 * 1000).toISOString(), - entries: [ - { - name: 'ci.yml', - path: '.github/workflows/ci.yml', - type: 'file', - lastCommitAt: new Date(Date.now() - 2500 * 1000).toISOString(), - lastCommitMessage: 'ci: mock workflow', - lastCommitSha: 'mock789' - } - ] - } + { name: 'CODEOWNERS', path: '.github/CODEOWNERS', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'add CODEOWNERS' }, + { name: 'workflows', path: '.github/workflows', type: 'dir', lastCommitAt: new Date().toISOString(), entries: [ + { name: 'ci.yml', path: '.github/workflows/ci.yml', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'ci: add' } + ] } ] }; -export default function SafeSettingsHubContent() { +export default function SafeSettingsHubContent3b() { const hydrated = useHydrated(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [rootTree, setRootTree] = useState(null); // recursive tree response + const [rootTree, setRootTree] = useState(null); const [search, setSearch] = useState(''); - // Tree view removed; we now render a flattened table. + const [expandedPaths, setExpandedPaths] = useState(() => new Set()); + const [selectedPath, setSelectedPath] = useState(null); const [lastFetchedAt, setLastFetchedAt] = useState(null); - const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); // direction: 'asc' | 'desc' | null - const [expandedPaths, setExpandedPaths] = useState(() => new Set()); // which directory paths are expanded const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - // Always ask for recursive tree; server may limit depth - // Explicitly request content bodies (fetchContent=true is default but sent for clarity) - fetch('/api/safe-settings-hub/content?fetchContent=true') + fetch('/api/safe-settings-hub/content?fetchContent=true') .then(r => { - if (!r.ok) { - // Surface a clear error message instead of falling back to mock data - throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status}). Please try again later.`); - } + if (!r.ok) throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status})`); return r.json(); }) - .then(json => { - // On success set the returned tree - setRootTree(json); - setLastFetchedAt(new Date()); - }) - .catch(e => setError(e.message)) + .then(json => { setRootTree(json); setLastFetchedAt(new Date()); }) + .catch(() => setRootTree(MOCK_TREE)) .finally(() => setLoading(false)); }; useEffect(() => { fetchData(); }, [hydrated]); - // Flatten nodes for table display - const flattenNodes = useCallback((node, acc = [], depth = 0) => { - if (!node) return acc; - acc.push({ - name: node.name, - path: node.path, - type: node.type, - lastCommitAt: node.lastCommitAt, - lastCommitMessage: node.lastCommitMessage, - lastCommitSha: node.lastCommitSha, - depth - }); - if (node.type === 'dir' && Array.isArray(node.entries)) { - node.entries.forEach(child => flattenNodes(child, acc, depth + 1)); - } - return acc; - }, []); - const filterTree = useCallback((node) => { if (!node) return null; const term = search.toLowerCase(); - const matches = (n) => !term || n.name.toLowerCase().includes(term) || n.path.toLowerCase().includes(term); - if (node.type === 'file') { - return matches(node) ? node : null; - } + const matches = (n) => !term || (n.name && n.name.toLowerCase().includes(term)) || (n.path && n.path.toLowerCase().includes(term)); + if (node.type === 'file') return matches(node) ? node : null; if (node.type === 'dir') { const children = (node.entries || []).map(filterTree).filter(Boolean); - if (matches(node) || children.length > 0) { - return { ...node, entries: children }; - } + if (matches(node) || children.length) return { ...node, entries: children }; return null; } return null; @@ -117,25 +59,18 @@ export default function SafeSettingsHubContent() { const filteredTree = useMemo(() => filterTree(rootTree), [rootTree, filterTree]); - // If the root contains a top-level 'safe-settings' directory, treat that directory as the display root const displayTree = useMemo(() => { if (!filteredTree) return null; if (filteredTree.type === 'dir') { const nameMatch = (n) => n && n.type === 'dir' && n.name && n.name.toLowerCase().includes('safe-settings'); - // Prefer immediate child named 'safe-settings' const immediate = (filteredTree.entries || []).find(nameMatch); if (immediate) return immediate; - // Fallback: search descendants up to a small depth for 'safe-settings' const findDescendant = (node, depth = 0, maxDepth = 3) => { if (!node || node.type !== 'dir' || depth >= maxDepth) return null; - for (const child of node.entries || []) { - if (nameMatch(child)) return child; - } - for (const child of node.entries || []) { - if (child.type === 'dir') { - const found = findDescendant(child, depth + 1, maxDepth); - if (found) return found; - } + for (const child of node.entries || []) if (nameMatch(child)) return child; + for (const child of node.entries || []) if (child.type === 'dir') { + const found = findDescendant(child, depth + 1, maxDepth); + if (found) return found; } return null; }; @@ -145,268 +80,205 @@ export default function SafeSettingsHubContent() { return filteredTree; }, [filteredTree]); - // When a search filter is applied, auto-expand all ancestor directories that contain matches - useEffect(() => { - if (!search) return; // only on active filter - if (!displayTree || displayTree.type !== 'dir') return; - const dirsToExpand = new Set(); - const walk = (node) => { - if (!node || node.type !== 'dir') return false; - let containsMatch = false; + useEffect(() => { if (!displayTree) return; setSelectedPath(prev => prev || displayTree.path); }, [displayTree]); + + const findNodeByPath = useCallback((node, path) => { + if (!node) return null; + if (node.path === path) return node; + if (node.type === 'dir') { for (const child of node.entries || []) { - if (child.type === 'dir') { - if (walk(child)) { - containsMatch = true; - dirsToExpand.add(child.path); // expand child dir to show deeper matches - } - } else { - // Any file present means this dir should be opened if it passed filtering - containsMatch = true; - } + const found = findNodeByPath(child, path); + if (found) return found; } - return containsMatch; - }; - walk(displayTree); - // Also expand top-level dirs that survived filtering and have entries - (displayTree.entries || []).forEach(e => { if (e.type === 'dir') dirsToExpand.add(e.path); }); - setExpandedPaths(prev => { - const next = new Set(prev); - dirsToExpand.forEach(p => next.add(p)); - return next; - }); - }, [search, displayTree]); - - const flatList = useMemo(() => { - if (!displayTree) return []; - // If display root is a directory, list its children instead of the directory itself (hide intermediate root) - if (displayTree.type === 'dir') { - return displayTree.entries.flatMap(child => flattenNodes(child, [], 0)); } - return flattenNodes(displayTree, [], 0); - }, [displayTree, flattenNodes]); + return null; + }, []); - // Build hierarchical visible list honoring expandedPaths and optional sorting - const sortedFlatList = useMemo(() => { - if (!displayTree) return []; - // function to sort entries inside a directory when sorting enabled - const sortEntries = (entries) => { - if (!sortConfig.key || !sortConfig.direction) return entries; - const key = sortConfig.key; - return [...entries].sort((a, b) => { - let av; let bv; - switch (key) { - case 'name': av = a.name.toLowerCase(); bv = b.name.toLowerCase(); break; - case 'path': av = a.path.toLowerCase(); bv = b.path.toLowerCase(); break; - case 'lastCommitAt': av = a.lastCommitAt ? new Date(a.lastCommitAt).getTime() : 0; bv = b.lastCommitAt ? new Date(b.lastCommitAt).getTime() : 0; break; - default: av = a[key]; bv = b[key]; - } - if (av < bv) return sortConfig.direction === 'asc' ? -1 : 1; - if (av > bv) return sortConfig.direction === 'asc' ? 1 : -1; - return 0; - }); - }; - const out = []; - const process = (node, depth) => { - if (!node) return; - if (node.type === 'dir') { - const children = sortEntries(node.entries || []); - children.forEach(child => { - out.push({ - name: child.name, - path: child.path, - type: child.type, - lastCommitAt: child.lastCommitAt, - lastCommitMessage: child.lastCommitMessage, - lastCommitSha: child.lastCommitSha, - depth - }); - if (child.type === 'dir' && expandedPaths.has(child.path)) { - process(child, depth + 1); - } - }); - } else { - out.push({ - name: node.name, - path: node.path, - type: node.type, - lastCommitAt: node.lastCommitAt, - lastCommitMessage: node.lastCommitMessage, - lastCommitSha: node.lastCommitSha, - depth - }); - } - }; - // Start processing at displayTree (hiding any intermediate 'safe-settings' wrapper) - process(displayTree, 0); - return out; - }, [displayTree, sortConfig, expandedPaths]); + const selectedNode = useMemo(() => { + if (!displayTree || !selectedPath) return null; + return findNodeByPath(displayTree, selectedPath); + }, [displayTree, selectedPath, findNodeByPath]); - const cycleSort = (key) => { - setSortConfig(prev => { - if (prev.key === key) { - if (prev.direction === 'asc') return { key, direction: 'desc' }; - if (prev.direction === 'desc') return { key: null, direction: null }; // clear - } - return { key, direction: 'asc' }; - }); - }; + const toggleDir = (path) => { setExpandedPaths(prev => { const next = new Set(prev); if (next.has(path)) next.delete(path); else next.add(path); return next; }); }; - const renderSortIcon = (key) => { - if (sortConfig.key !== key) return ; - if (sortConfig.direction === 'asc') return ; - if (sortConfig.direction === 'desc') return ; - return ; + const formatTimeAgo = (iso) => { + if (!iso) return '—'; + const dt = new Date(iso); + if (Number.isNaN(dt.getTime())) return iso; + const diffSec = Math.floor((Date.now() - dt.getTime()) / 1000); + if (diffSec < 60) return 'just now'; + const diffMin = Math.floor(diffSec / 60); + if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`; + const diffH = Math.floor(diffMin / 60); + if (diffH < 24) return `${diffH} hour${diffH === 1 ? '' : 's'} ago`; + const diffD = Math.floor(diffH / 24); + if (diffD < 30) return `${diffD} day${diffD === 1 ? '' : 's'} ago`; + const diffM = Math.floor(diffD / 30); + if (diffM < 12) return diffM === 1 ? '1 month ago' : `${diffM} months ago`; + const diffY = Math.floor(diffD / 365); + if (diffY === 1) return 'last year'; + return `${diffY} years ago`; }; - const toggleDir = (path) => { - setExpandedPaths(prev => { - const next = new Set(prev); - if (next.has(path)) next.delete(path); else next.add(path); - return next; - }); - }; + const repoCount = useMemo(() => { + if (!rootTree) return '—'; + const rp = rootTree.reposProcessed || rootTree.repos || null; + if (!rp) return '—'; + if (Array.isArray(rp)) return rp.length; + if (typeof rp === 'object') return Object.keys(rp).length; + return '—'; + }, [rootTree]); - const collectAllDirPaths = useCallback((node, acc = []) => { - if (!node) return acc; - if (node.type === 'dir') { - if (node.path && node.path !== '.github') acc.push(node.path); // skip synthetic root label - (node.entries || []).forEach(child => collectAllDirPaths(child, acc)); + const renderTree = (node, depth = 0) => { + if (!node) return null; + if (node.type === 'file') { + const selected = selectedPath === node.path; + return ( +
setSelectedPath(node.path)}> + + {node.name} +
+ ); } - return acc; - }, []); - - const expandAll = () => { - if (!filteredTree) return; - const all = collectAllDirPaths(filteredTree, []); - setExpandedPaths(new Set(all)); + const expanded = expandedPaths.has(node.path); + const selected = selectedPath === node.path; + return ( +
+
+
{ toggleDir(node.path); setSelectedPath(node.path); }} className="d-inline-flex align-items-center"> + {expanded ? : } + + {node.name} +
+
+ {expanded && (node.entries || []).map(child => renderTree(child, depth + 1))} +
+ ); }; - const collapseAll = () => setExpandedPaths(new Set()); + const childrenForSelected = useMemo(() => { if (!selectedNode) return []; if (selectedNode.type === 'dir') return selectedNode.entries || []; return []; }, [selectedNode]); - const formatRelative = (iso) => { - if (!iso) return null; - const dt = new Date(iso); - let diffSec = Math.floor((Date.now() - dt.getTime()) / 1000); - if (diffSec < 0) diffSec = 0; - if (diffSec < 60) return '0m'; - const mTotal = Math.floor(diffSec / 60); - if (mTotal < 60) return `${mTotal}m`; - const hTotal = Math.floor(mTotal / 60); - if (hTotal < 24) { - const remM = mTotal % 60; - return remM ? `${hTotal}h ${remM}m` : `${hTotal}h`; - } - const dTotal = Math.floor(hTotal / 24); - const remH = hTotal % 24; - return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; - }; + const fileContent = useMemo(() => { if (!selectedNode || selectedNode.type !== 'file') return null; return selectedNode.content || selectedNode.body || selectedNode.text || selectedNode.preview || null; }, [selectedNode]); - // Table columns: Name (indented), Path, Type, Last update + const fileLines = useMemo(() => fileContent ? fileContent.split('\n') : [], [fileContent]); + const lineCount = fileLines.length; + const locCount = fileLines.filter(l => l.trim()).length; + const byteCount = useMemo(() => { + if (!fileContent) return 0; + try { return new TextEncoder().encode(fileContent).length; } catch (e) { return fileContent.length; } + }, [fileContent]); return ( - <>
-
-
-
- - - - setSearch(e.target.value)} /> -
-
-
-
- - - -
-
-
- - {/*
-
-
-
- - - - setSearchTerm(e.target.value)} - /> +
+
+
+
+ + setSearch(e.target.value)} />
+ {selectedNode &&
{selectedNode.path}
}
-
- - Showing {sortedData.length} of {data.length} organizations - +
+
+
+ {/* edit button intentionally removed */}
-
*/} - +
+ {loading &&
Loading…
} - {error && !loading &&
Error: {error}
} - {!loading && !error && !displayTree &&
No entries
} + {error &&
{error}
} + {!loading && !displayTree &&
No entries
} + + {!loading && displayTree && ( +
+
+ {/* left tree */} + {displayTree.type === 'dir' && displayTree.name && displayTree.name.toLowerCase().includes('safe-settings') + ? (displayTree.entries || []).map(child => renderTree(child, 0)) + : renderTree(displayTree) + } +
- {!loading && !error && displayTree && ( -
-
- - - - - - - - - - {sortedFlatList.map(node => { - const isDir = node.type === 'dir'; - const expanded = isDir && expandedPaths.has(node.path); - return ( - isDir && toggleDir(node.path)} style={isDir ? { cursor: 'pointer' } : undefined}> - - - - - ); - })} - -
cycleSort('name')} className="theme-text-primary user-select-none" style={{ width: '35%', cursor: 'pointer' }}>Name {renderSortIcon('name')} cycleSort('path')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Path {renderSortIcon('path')} cycleSort('lastCommitAt')} className="theme-text-primary user-select-none" style={{ width: '170px', cursor: 'pointer' }}>Last update {renderSortIcon('lastCommitAt')}
- - {isDir ? ( - expanded ? : +
+ {/* right content (dir/file view) */} + {selectedNode && selectedNode.type === 'dir' && ( +
+ {/* path rendered next to the filter at the top; removed empty toolbar to avoid extra top gap */} +
+ + + + + + + + + + {(childrenForSelected.length === 0) && ( + + )} + {childrenForSelected.map(child => ( + setSelectedPath(child.path)}> + + + + + ))} + +
NameCommit-MessageLast commit date
No entries
+ + {child.type === 'dir' ? : } + {child.name} + + {child.lastCommitMessage || '—'}{child.lastCommitAt ? formatTimeAgo(child.lastCommitAt) : '—'}
+
+
+ )} + + {selectedNode && selectedNode.type === 'file' && ( +
+ {/* path rendered next to the filter at the top; removed empty toolbar to avoid extra top gap */} +
+ {/* file header with border and rounded top, followed by a bordered code area with rounded bottom */} +
+
+
+
+ + +
+
+ +
+
+
+ {fileLines.map((_, i) =>
{i + 1}
)} +
+
+ {fileLines.length === 0 ? ( +
No content available
) : ( - + fileLines.map((ln, i) =>
{ln || ' '}
) )} - {isDir && } - {node.name} - -
{node.path} - {node.lastCommitAt ? formatRelative(node.lastCommitAt) : '—'} -
+
+
+
+
+
+
+ )} + + {!selectedNode && ( +
Select a folder or file from the left to view contents.
+ )} +
)} + + {/* footer (items shown) removed */} - {!loading && !error && ( -
- {sortedFlatList.length} items shown - {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} -
- )} - {/* Removed inner bordered wrapper styles so only outer page container shows a border */} - ); } diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx index be57b934e..818f4a356 100644 --- a/ui/src/app/components/TitleBar.jsx +++ b/ui/src/app/components/TitleBar.jsx @@ -1,7 +1,7 @@ "use client"; import { usePathname } from "next/navigation"; import React from "react"; -import { GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; +import { GlobeIcon, GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; import { useTheme } from './ThemeContext'; import './TitleBar.css'; @@ -37,13 +37,13 @@ export default function TitleBar() {
  • - + - Organizations - {pathname === "/dashboard/organizations" && ( + Safe-Settings Hub + {pathname === "/dashboard/safe-settings-hub" && ( )} @@ -51,13 +51,13 @@ export default function TitleBar() {
  • - + - Safe-Settings Hub - {pathname === "/dashboard/safe-settings-hub" && ( + Organizations + {pathname === "/dashboard/organizations" && ( )} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index d4fb0de01..46564ec39 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -20,7 +20,7 @@ } [data-theme="dark"] { - --bg-primary: rgb(13,17,22); + --bg-primary: rgb(13, 17, 22); --bg-secondary: #444444; --bg-accent: #30363d; --text-primary: #f0f6fc; @@ -86,7 +86,8 @@ body.light-theme .nav-link { [data-theme="dark"] .nav-link, body.dark-theme .nav-link { - color: #f6f8fa !important; + /* color: #f6f8fa !important; */ + color: #6c757d !important; } /* title bar nav tabs */ @@ -104,9 +105,9 @@ body.light-theme .nav-tabs { /* Apply theme variables to main element */ main { color: var(--text-primary) !important; - padding: 1rem; + /* padding: 1rem; */ border-radius: 12px; - margin-top: 1rem; + /* margin-top: 1rem; */ } /* Theme Utility Classes */ @@ -135,12 +136,15 @@ main { } .theme-border { - border-color: var(--border-color) !important; /* override bootstrap .border */ + border-color: var(--border-color) !important; + /* override bootstrap .border */ } -.border.theme-border, .theme-border.border { +.border.theme-border, +.theme-border.border { border-color: var(--border-color) !important; } + /* Global Font Utility Classes */ .dark-font { color: #f6f8fa; @@ -209,7 +213,7 @@ main { background-color: var(--bg-accent) !important; } -.text-muted { +.text-muted { color: var(--text-secondary) !important; } @@ -225,6 +229,8 @@ element { color: var(--text-primary) !important; background-color: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; + margin-left: 10px !important; + gap: 1.5rem !important; } .input-group-text { @@ -241,20 +247,23 @@ element { } /* Env vars table dark mode override */ -[data-theme="dark"] .env-vars table, body.dark-theme .env-vars table { +[data-theme="dark"] .env-vars table, +body.dark-theme .env-vars table { background-color: var(--bg-primary) !important; } -[data-theme="dark"] .env-vars thead th, body.dark-theme .env-vars thead th { + +[data-theme="dark"] .env-vars thead th, +body.dark-theme .env-vars thead th { background-color: var(--bg-secondary) !important; } -th{ +th { font-weight: 600; background-color: var(--bg-secondary) !important; } -tr td{ +tr td { background-color: var(--bg-primary) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; -} +} \ No newline at end of file From 498bbbd13d93c0dc576225c890ac81308a8c33c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Tue, 9 Sep 2025 23:04:26 -0400 Subject: [PATCH 3/5] hub improvements --- docs/hubSyncHandler/README.md | 41 +- index.js | 98 +- lib/hubSyncHandler.js | 684 +++++++++-- lib/installationCache.js | 78 +- lib/mergeDeep.js | 4 +- lib/plugins/archive.js | 95 +- lib/plugins/branches.js | 8 +- lib/plugins/environments.js | 2 +- lib/plugins/overrides.js | 16 +- lib/plugins/rulesets.js | 10 +- lib/routes.js | 340 ++++-- lib/settings.js | 18 +- package-lock.json | 1054 ++++++++++++++++- package.json | 4 +- test/unit/index.test.js | 10 +- test/unit/lib/hubSyncHandler.test.js | 106 ++ test/unit/lib/plugins/archive.test.js | 84 +- test/unit/lib/plugins/environments.test.js | 58 +- test/unit/lib/plugins/rulesets.test.js | 30 +- test/unit/lib/routes.test.js | 140 +++ test/unit/lib/settings.test.js | 129 +- ui/README.md | 29 + ui/favico.ico | Bin 3961 -> 0 bytes ui/favico.png | Bin 3961 -> 0 bytes ui/favicon.svg | 3 + ui/next.config.js | 9 + ui/package-lock.json | 100 +- ui/package.json | 8 +- ui/public/favicon.svg | 2 +- ui/public/shield.png | Bin 24289 -> 0 bytes ui/shield.png | Bin 3961 -> 4883 bytes ui/shield.svg | 3 + ui/src/app/components/EnvVariables.jsx | 20 +- ui/src/app/components/HubOrgGraph.jsx | 140 +++ ui/src/app/components/OrganizationsTable.jsx | 508 ++++++-- .../components/Safe-settings-hubContent.jsx | 28 +- ui/src/app/components/TitleBar.css | 6 +- ui/src/app/components/TitleBar.jsx | 88 +- ui/src/app/dashboard/help/page.jsx | 35 + ui/src/app/dashboard/organizations/page.jsx | 2 +- ui/src/app/dashboard/page.jsx | 12 + .../app/dashboard/safe-settings-hub/page.jsx | 2 +- ui/src/app/dashboard/settings/page.jsx | 13 - ui/src/app/globals.css | 24 +- ui/src/app/route.js | 7 - 45 files changed, 3334 insertions(+), 714 deletions(-) create mode 100644 test/unit/lib/hubSyncHandler.test.js create mode 100644 test/unit/lib/routes.test.js delete mode 100644 ui/favico.ico delete mode 100644 ui/favico.png create mode 100644 ui/favicon.svg delete mode 100644 ui/public/shield.png create mode 100644 ui/shield.svg create mode 100644 ui/src/app/components/HubOrgGraph.jsx create mode 100644 ui/src/app/dashboard/help/page.jsx delete mode 100644 ui/src/app/dashboard/settings/page.jsx delete mode 100644 ui/src/app/route.js diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index 4f4d06c80..df08f0f6f 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -44,13 +44,40 @@ Environment variables specific to the 'Sync-Feature' 3. _`'Global'`_ `Hub Admin Repo` updates. Changes will `applied to all Organization` +--- -```mermaid -flowchart LR -PR --> Hub -Hub --> ORG-A -Hub -..- ORG-B -Hub -..- ORG-C +## Safe-Settings Hub API endpoints + +### API Endpoints + +The following table summarizes the Safe Settings API endpoints: + +| Endpoint | Method | Purpose | Example Usage | +|------------------------------------------|--------|------------------------------------------------------|---------------| +| `/api/safe-settings/installation` | GET | Organization installation, repo, and sync status | Fetch org status | +| `/api/safe-settings/hub/content` | GET | List hub repo files/directories | List hub files | +| `/api/safe-settings/hub/content/*` | GET | Fetch specific file or directory from hub repo | Get file content | +| `/api/safe-settings/hub/import` | POST | Import settings from orgs into the hub | Import org settings | +| `/api/safe-settings/env` | GET | App environment/config variables | Get env vars | + +**Examples:** +- Fetch org installation status: + ```http + GET /api/safe-settings/installation + ``` +- Import settings from orgs: + ```http + POST /api/safe-settings/hub/import + Body: { "orgs": ["org1", "org2"] } + ``` +- List hub repo files: + ```http + GET /api/safe-settings/hub/content?ref=main&recursive=true + ``` +- Get environment variables: + ```http + GET /api/safe-settings/env + ``` +--- -``` \ No newline at end of file diff --git a/index.js b/index.js index e4c809f66..265965183 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,86 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => // Initialize installation cache (env-controlled prefetch) initCache(robot) - async function renameSync(nop, context, repo = context.repo(), rename, ref) { + async function syncAllSettings (nop, context, repo = context.repo(), ref) { + try { + deploymentConfig = await loadYamlFileSystem() + robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) + const configManager = new ConfigManager(context, ref) + const runtimeConfig = await configManager.loadGlobalSettingsYaml() + const config = Object.assign({}, deploymentConfig, runtimeConfig) + robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) + if (ref) { + return Settings.syncAll(nop, context, repo, config, ref) + } else { + return Settings.syncAll(nop, context, repo, config) + } + } catch (e) { + if (nop) { + let filename = env.SETTINGS_FILE_PATH + if (!deploymentConfig) { + filename = env.DEPLOYMENT_CONFIG_FILE_PATH + deploymentConfig = {} + } + const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + } else { + throw e + } + } + } + + async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { + try { + deploymentConfig = await loadYamlFileSystem() + robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) + const configManager = new ConfigManager(context, ref) + const runtimeConfig = await configManager.loadGlobalSettingsYaml() + const config = Object.assign({}, deploymentConfig, runtimeConfig) + robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) + return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref) + } catch (e) { + if (nop) { + let filename = env.SETTINGS_FILE_PATH + if (!deploymentConfig) { + filename = env.DEPLOYMENT_CONFIG_FILE_PATH + deploymentConfig = {} + } + const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + } else { + throw e + } + } + } + + async function syncSettings (nop, context, repo = context.repo(), ref) { + try { + deploymentConfig = await loadYamlFileSystem() + robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) + const configManager = new ConfigManager(context, ref) + const runtimeConfig = await configManager.loadGlobalSettingsYaml() + const config = Object.assign({}, deploymentConfig, runtimeConfig) + robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) + return Settings.sync(nop, context, repo, config, ref) + } catch (e) { + if (nop) { + let filename = env.SETTINGS_FILE_PATH + if (!deploymentConfig) { + filename = env.DEPLOYMENT_CONFIG_FILE_PATH + deploymentConfig = {} + } + const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + } else { + throw e + } + } + } + + async function renameSync (nop, context, repo = context.repo(), rename, ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -46,14 +125,13 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } } - /** * Loads the deployment config file from file system * Do this once when the app starts and then return the cached value * * @return The parsed YAML file */ - async function loadYamlFileSystem() { + async function loadYamlFileSystem () { if (deploymentConfig === undefined) { const deploymentConfigPath = env.DEPLOYMENT_CONFIG_FILE_PATH if (fs.existsSync(deploymentConfigPath)) { @@ -65,7 +143,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return deploymentConfig } - function getAllChangedSubOrgConfigs(payload) { + function getAllChangedSubOrgConfigs (payload) { const pattern = Settings.SUB_ORG_PATTERN const getMatchingFiles = (commits, type) => @@ -82,7 +160,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getAllChangedRepoConfigs(payload, owner) { + function getAllChangedRepoConfigs (payload, owner) { const pattern = Settings.REPO_PATTERN const getMatchingFiles = (commits, type) => @@ -99,7 +177,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedRepoConfigName(files, owner) { + function getChangedRepoConfigName (files, owner) { const pattern = Settings.REPO_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -110,7 +188,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedSubOrgConfigName(files) { + function getChangedSubOrgConfigName (files) { const pattern = Settings.SUB_ORG_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -120,7 +198,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => path: modifiedFile })) } - async function createCheckRun(context, pull_request, head_sha, head_branch) { + async function createCheckRun (context, pull_request, head_sha, head_branch) { const { payload } = context // robot.log.debug(`Check suite was requested! for ${context.repo()} ${pull_request.number} ${head_sha} ${head_branch}`) const res = await context.octokit.checks.create({ @@ -132,7 +210,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => robot.log.debug(JSON.stringify(res, null)) } - async function info() { + async function info () { const github = await robot.auth() const installations = await github.paginate( github.apps.listInstallations.endpoint.merge({ per_page: 100 }) @@ -147,7 +225,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncInstallation(nop = false) { + async function syncInstallation (nop = false) { robot.log.trace('Fetching installations') const github = await robot.auth() diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js index c34de6d66..8ef694afe 100644 --- a/lib/hubSyncHandler.js +++ b/lib/hubSyncHandler.js @@ -1,6 +1,21 @@ const env = require('./env') const { getInstallations } = require('./installationCache') +/** + * Get authenticated octokit client for an org installation + * @param {import('probot').Probot} robot + * @param {string} orgName + * @returns {Promise} Authenticated client or null + */ +async function getOrgInstallation (robot, orgName) { + const installs = await getInstallations(robot) + const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === orgName.toLowerCase()) + if (!install) { + return null + } + return await robot.auth(install.id) +} + /** * Sync changed safe-settings organization files from the master admin PR * into the target organization's admin repository. @@ -10,178 +25,176 @@ const { getInstallations } = require('./installationCache') * @param {string} destRepo Destination repo name inside orgName (e.g. admin repo) * @param {string} destinationFolder Base folder in destination repo where content lives (e.g. .github or .github/safe-settings) */ -async function syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder) { +async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationFolder) { try { - robot.log.info(`Syncing safe settings for organization: ${orgName}`); + robot.log.info(`Syncing safe settings for organization: ${orgName}`) - robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`); - const pr = context.payload.pull_request; + robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`) + const pr = context.payload.pull_request if (!pr) { - robot.log.warn('No pull_request payload found; aborting sync'); - return; + robot.log.warn('No pull_request payload found; aborting sync') + return } - const { owner: srcOwner, repo: srcRepo } = context.repo(); - const pull_number = pr.number; + const { owner: srcOwner, repo: srcRepo } = context.repo() + const pull_number = pr.number // Source base path where org folders live inside master admin repo // 'safe-settings' is the standard sub-folder path - const configRoot = env.CONFIG_PATH || '.github/'; - const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, ''); - robot.log.info(`DEBUG: sourceBase='${sourceBase}'`); + const configRoot = env.CONFIG_PATH || '.github/' + const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, '') + robot.log.info(`DEBUG: sourceBase='${sourceBase}'`) // Debug info: log env and computed paths - robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`); + robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`) // List changed files in PR const files = await context.octokit.paginate( context.octokit.rest.pulls.listFiles, { owner: srcOwner, repo: srcRepo, pull_number, per_page: 100 } - ); + ) - robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`); - if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); + robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`) + if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) // Dump file objects for debugging filename issues if (files.length) { try { - robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`); - robot.log.info(`DEBUG: file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`); + robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`) + robot.log.info(`DEBUG: file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`) } catch (e) { - robot.log.info(`DEBUG: failed to stringify first file: ${e.message}`); + robot.log.info(`DEBUG: failed to stringify first file: ${e.message}`) } files.forEach((f, i) => { try { - robot.log.info(`DEBUG: FILE[${i}] raw=${JSON.stringify(f)}`); - robot.log.info(`DEBUG: FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`); + robot.log.info(`DEBUG: FILE[${i}] raw=${JSON.stringify(f)}`) + robot.log.info(`DEBUG: FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`) } catch (e) { - robot.log.info(`DEBUG: FILE[${i}] stringify error: ${e.message}`); + robot.log.info(`DEBUG: FILE[${i}] stringify error: ${e.message}`) } - }); + }) } - const orgPrefix = `${sourceBase}/${orgName}/`; - robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); - robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`); - const relevant = files.filter(f => f.filename === `${sourceBase}/${orgName}` || f.filename.startsWith(orgPrefix)); - robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`); + const orgPrefix = `${sourceBase}/${orgName}/` + robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) + robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`) + const relevant = files.filter(f => f.filename === `${sourceBase}/${orgName}` || f.filename.startsWith(orgPrefix)) + robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`) if (!relevant.length) { - robot.log.info(`No files for org ${orgName} in PR #${pull_number}`); + robot.log.info(`No files for org ${orgName} in PR #${pull_number}`) // Detailed per-file checks to help debug matching files.forEach(f => { - const exact = f.filename === `${sourceBase}/${orgName}`; - const pref = f.filename.startsWith(orgPrefix); - robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`); - }); + const exact = f.filename === `${sourceBase}/${orgName}` + const pref = f.filename.startsWith(orgPrefix) + robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`) + }) // Also show alternate check using CONFIG_PATH + '/organizations' - const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations`; - const altPrefix = `${altBase}/${orgName}/`; + const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations` + const altPrefix = `${altBase}/${orgName}/` files.forEach(f => { - const exactAlt = f.filename === `${altBase}/${orgName}`; - const prefAlt = f.filename.startsWith(altPrefix); - robot.log.info(`ALT CHECK: file='${f.filename}' exactAlt=${exactAlt} prefAlt=${prefAlt}`); - }); - return; + const exactAlt = f.filename === `${altBase}/${orgName}` + const prefAlt = f.filename.startsWith(altPrefix) + robot.log.info(`ALT CHECK: file='${f.filename}' exactAlt=${exactAlt} prefAlt=${prefAlt}`) + }) + return } // Destination info - const destOwner = orgName; + const destOwner = orgName // ensure destBase uses the configured CONFIG_PATH (fallback to '.github') and normalize trailing slash - const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, ''); - const destBaseBranch = 'main'; - const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1'); + const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, '') + const destBaseBranch = 'main' + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') - // Find installation for destination org to auth - const installs = await getInstallations(robot) - const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === destOwner.toLowerCase()); - if (!install) { - robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`); - return; + // Find installation for destination org to auth (reusable helper) + const githubDest = await getOrgInstallation(robot, destOwner) + if (!githubDest) { + robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`) + return } - const githubDest = await robot.auth(install.id); - robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`); + robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`) // Create branch if not direct push - const timestamp = Date.now(); - const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}`; + const timestamp = Date.now() + const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}` if (!directPush) { try { - const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }); - const baseSha = baseRef.data.object.sha; - await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }); - robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`); + const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }) + const baseSha = baseRef.data.object.sha + await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) + robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`) } catch (err) { if (err.status === 422) { - robot.log.warn(`Branch ${branchName} already exists, continuing`); + robot.log.warn(`Branch ${branchName} already exists, continuing`) } else { - throw err; + throw err } } } for (const f of relevant) { - let relative; + let relative if (f.filename === `${sourceBase}/${orgName}`) { // top directory marker encountered (unlikely in changed files list) - skip - continue; + continue } else { - relative = f.filename.slice(orgPrefix.length); + relative = f.filename.slice(orgPrefix.length) } // place only the changed file under the configured CONFIG_PATH (e.g. '.github/') - const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/'); + const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/') try { - const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }); - const data = srcContentResp.data; + const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }) + const data = srcContentResp.data if (Array.isArray(data)) { // Skip directories; individual files will appear separately in changed files list - continue; + continue } - const fileContent = Buffer.from(data.content, data.encoding).toString('utf8'); - const encoded = Buffer.from(fileContent, 'utf8').toString('base64'); + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') + const encoded = Buffer.from(fileContent, 'utf8').toString('base64') // Check existing file for sha - let existingSha = undefined; + let existingSha try { - const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }); - if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha; + const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }) + if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha } catch (getErr) { - if (getErr.status !== 404) throw getErr; // ignore missing + if (getErr.status !== 404) throw getErr // ignore missing } await githubDest.rest.repos.createOrUpdateFileContents({ owner: destOwner, repo: destRepo, - path: destPath, + path: destPath, message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, content: encoded, branch: branchName, sha: existingSha, committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } - }); - robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`); + }) + robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`) } catch (fileErr) { - robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`); - throw fileErr; + robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`) + throw fileErr } } if (!directPush) { try { - const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`; - const prBody = `Automated sync of safe-settings for ${orgName} from ${srcOwner}/${srcRepo} PR #${pull_number}.`; - const created = await githubDest.rest.pulls.create({ owner: destOwner, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }); - robot.log.info(`Created PR ${created.data.html_url} in ${destOwner}/${destRepo}`); + const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` + const prBody = `Automated sync of safe-settings for ${orgName} from ${srcOwner}/${srcRepo} PR #${pull_number}.` + const created = await githubDest.rest.pulls.create({ owner: destOwner, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }) + robot.log.info(`Created PR ${created.data.html_url} in ${destOwner}/${destRepo}`) } catch (prErr) { - robot.log.error(`Failed to create PR in ${destOwner}/${destRepo}: ${prErr.message}`); - throw prErr; + robot.log.error(`Failed to create PR in ${destOwner}/${destRepo}: ${prErr.message}`) + throw prErr } } else { - robot.log.info(`Changes pushed directly to ${destOwner}/${destRepo}@${destBaseBranch}`); + robot.log.info(`Changes pushed directly to ${destOwner}/${destRepo}@${destBaseBranch}`) } } catch (err) { - robot.log.error(`syncSafeSettingConfig error for org ${orgName}: ${err.message}`); + robot.log.error(`syncSafeSettingConfig error for org ${orgName}: ${err.message}`) } } @@ -191,68 +204,493 @@ async function syncSafeSettingConfig(robot, context, orgName, destRepo, destinat * @param {import('probot').Probot} robot * @param {import('probot').Context} context */ -async function hubSyncHandler(robot, context) { - const { payload } = context; - const { repository, pull_request } = payload || {}; - robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`); +async function hubSyncHandler (robot, context) { + const { payload } = context + const { repository, pull_request } = payload || {} + robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`) try { // Ensure the event is from the configured Safe-Settings Hub repo/org - const isMasterRepo = repository && repository.name === env.SAFE_SETTINGS_HUB_REPO; - const isMasterOrg = repository && repository.owner && repository.owner.login === env.SAFE_SETTINGS_HUB_ORG; + const isMasterRepo = repository && repository.name === env.SAFE_SETTINGS_HUB_REPO + const isMasterOrg = repository && repository.owner && repository.owner.login === env.SAFE_SETTINGS_HUB_ORG if (!(isMasterRepo && isMasterOrg)) { - robot.log.info(`Pull request.closed is not from master admin repo/org (${env.SAFE_SETTINGS_HUB_ORG}/${env.SAFE_SETTINGS_HUB_REPO}), ignoring`); - return; + robot.log.info(`Pull request.closed is not from master admin repo/org (${env.SAFE_SETTINGS_HUB_ORG}/${env.SAFE_SETTINGS_HUB_REPO}), ignoring`) + return } - robot.log.info(`Pull request closed on Safe-Settings Hub: (${repository.full_name})`); + robot.log.info(`Pull request closed on Safe-Settings Hub: (${repository.full_name})`) // Get the PR details - const pr = pull_request; - const { owner, repo } = context.repo(); - const pull_number = pr.number; - const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`; + const pr = pull_request + const { owner, repo } = context.repo() + const pull_number = pr.number // Paginate through all files changed in the PR const files = await context.octokit.paginate( context.octokit.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 } - ); + ) - robot.log.info(`Files changed in PR #${pull_number}: ${files.map(f => f.filename).join(', ')}`); + robot.log.info(`Files changed in PR #${pull_number}: ${files.map(f => f.filename).join(', ')}`) - // Normalize baseSettingsPath (remove trailing slash if any) - const normalizedBase = baseSettingsPath.replace(/\/$/, ''); - robot.log.debug(`Normalized base path: ${normalizedBase}`); + // Routing logic: check for 'globals' or 'organizations' folder changes + const globalsChanged = files.some(f => /\/globals\//.test(f.filename)) + const orgsChanged = files.some(f => /\/organizations\//.test(f.filename)) - // Escape string for use in RegExp - const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (globalsChanged) { + robot.log.info('Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...).') + await module.exports.syncHubGlobalsUpdate(robot, context, files) + } + + if (orgsChanged) { + robot.log.info('Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...).') + // Only sync updates in organization subfolders, not files directly in organizations folder + const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations` + const normalizedBase = baseSettingsPath.replace(/\/$/, '') + const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Only match files in org subfolders: .../organizations//... + const orgSubfolderPattern = new RegExp(`^${escapeRegex(normalizedBase)}/([^/]+)/.+`) + const orgNamesSet = new Set() + files.forEach(f => { + const m = f.filename.match(orgSubfolderPattern) + if (m && m[1]) { + orgNamesSet.add(m[1]) + } + }) + const orgNames = Array.from(orgNamesSet) + robot.log.info(`Orgs updated in PR #${pull_number}: ${orgNames.join(', ')}`) + for (const orgName of orgNames) { + const destRepo = env.ADMIN_REPO + const destinationFolder = env.CONFIG_PATH || '.github' + await module.exports.syncHubOrgUpdate(robot, context, orgName, destRepo, destinationFolder) + } + } + } catch (err) { + robot.log.error(`Failed to sync safe settings: ${err && err.message ? err.message : err}`) + } +} - // Build a RegExp that captures the first path segment after the base path - const basePattern = new RegExp(`^${escapeRegex(normalizedBase)}/([^/]+)(?:/|$)`); - robot.log.debug(`Base pattern for org matching: ${basePattern}`); +/** + * Handle updates in the globals folder and sync to destinations defined in manifest.yml rules + * @param {import('probot').Probot} robot + * @param {import('probot').Context} context + * @param {Array} files - Array of changed file objects from PR + */ +async function syncHubGlobalsUpdate (robot, context, files) { + robot.log.info('syncHubGlobalsUpdate: Processing globals folder changes.') + // Step 1: Load manifest.yml rules from the hub repo + const yaml = require('js-yaml') + const util = require('util') + const manifestPath = `${env.CONFIG_PATH}/${env.SAFE_SETTINGS_HUB_PATH}/globals/manifest.yml` + let manifest + try { + // Get manifest.yml from the hub repo (default branch: main) + const resp = await context.octokit.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: manifestPath, + ref: 'main' + }) - // Collect unique org names - const orgNamesSet = new Set(); - files.forEach(f => { - const m = f.filename.match(basePattern); - if (m && m[1]) { - orgNamesSet.add(m[1]); + const manifestContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') + manifest = yaml.load(manifestContent) + robot.log.info('Loaded manifest.yml rules from hub repo:' + JSON.stringify(manifest, null, 2)) + } catch (err) { + robot.log.error('Failed to load manifest.yml from hub repo:' + err.message) + return + } + // Step 2: Determine which update to sync where + // Find changed files in the globals folder + const changedGlobals = files.filter(f => /\/globals\//.test(f.filename)) + if (!changedGlobals.length) { + robot.log.info('No changed files in globals folder.') + return + } + + // For each changed file, match against manifest rules + for (const fileObj of changedGlobals) { + const fileName = fileObj.filename.split('/').pop() + // Prevent manifest.yml from being synced to organizations + if (fileName === 'manifest.yml') { + robot.log.info(`Skipping sync for manifest.yml (should only exist in hub)`) + continue + } + robot.log.info(`Evaluating globals file: ${fileObj.filename}`) + for (const rule of manifest.rules || []) { + // Check if file matches rule.files (glob match, simple * and exact) + const matchesFile = (rule.files || []).some(pattern => { + if (pattern === fileName) return true + if (pattern.startsWith('*') && fileName.endsWith(pattern.slice(1))) return true + if (pattern.endsWith('*') && fileName.startsWith(pattern.slice(0, -1))) return true + if (pattern.includes('*')) { + // Simple contains match for * + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') + return regex.test(fileName) + } + return false + }) + if (!matchesFile) continue + + // Determine target orgs + const targets = rule.targets || [] + robot.log.info(`Rule '${rule.name}' matches file '${fileName}'. Targets: ${targets.join(', ')}`) + // Step 3: handle mergeStrategy and actual sync + const mergeStrategy = rule.mergeStrategy || 'merge' + for (const orgPattern of targets) { + // For demo, treat '*' as all orgs, otherwise match orgs by pattern + let orgsToSync = [] + if (orgPattern === '*') { + // Get all org installations + const installs = await getInstallations(robot) + orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization').map(i => i.account.login) + } else if (orgPattern.endsWith('*')) { + // Prefix match + const prefix = orgPattern.slice(0, -1) + const installs = await getInstallations(robot) + orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization' && i.account.login.startsWith(prefix)).map(i => i.account.login) + } else { + orgsToSync = [orgPattern] + } + for (const orgName of orgsToSync) { + robot.log.info(`Preparing to sync file '${fileName}' to org '${orgName}' with mergeStrategy='${mergeStrategy}'`) + // Check if org has a safe-settings config repo (repo exists) + const destRepo = env.ADMIN_REPO + // use the octokit client authenticated for the hub installation + const githubDest = await getOrgInstallation(robot, orgName) + if (!githubDest) { + robot.log.info(`Skipping org ${orgName}: no installation found.`) + continue + } + let repoExists = false + try { + await githubDest.repos.get({ owner: orgName, repo: destRepo }) + repoExists = true + } catch (err) { + if (err.status === 404) { + robot.log.info(`Skipping org ${orgName}: config repo '${destRepo}' does not exist.`) + continue + } else { + throw err + } + } + if (!repoExists) continue + // Check if file exists in org's repo + const destPath = `${env.CONFIG_PATH}/${fileName}` + let exists = false + let existingContent = null + try { + robot.log.info(`Checking existence of ${destPath} in ${orgName}/${destRepo}`) + const resp = await githubDest.repos.getContent({ + owner: orgName, + repo: destRepo, + path: destPath, + ref: 'main' + }) + if (!Array.isArray(resp.data)) { + robot.log.info(`Found ${destPath} in ${orgName}/${destRepo}`) + exists = true + existingContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') + } + } catch (err) { + if (err.status === 404) { + robot.log.info(`File ${destPath} not found in ${orgName}/${destRepo} (this is fine for both merge strategies)`) + exists = false + existingContent = null + } else { + robot.log.info(`Error checking ${destPath} in ${orgName}/${destRepo}: ${err.message}`) + throw err + } + } + // Merge strategy logic + if (mergeStrategy === 'merge' && exists) { + robot.log.info(`Skipping sync of ${fileName} to ${orgName} (already exists & mergeStrategy=${mergeStrategy})`) + continue + } + // For overwrite or merge with no existing file, sync + robot.log.info(`Syncing ${fileName} to ${orgName} (mergeStrategy=${mergeStrategy})`) + // Actual sync logic: create or update file in org repo + try { + // Get source file content from hub repo (use PR head SHA if available, else main) + let srcContentResp + const pr = context.payload && context.payload.pull_request + const srcRef = pr && pr.head && pr.head.sha ? pr.head.sha : 'main' + srcContentResp = await context.octokit.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fileObj.filename, + ref: srcRef + }) + const data = srcContentResp.data + if (Array.isArray(data)) { + robot.log.info(`Skipping directory ${fileObj.filename}`) + continue + } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') + const encoded = Buffer.from(fileContent, 'utf8').toString('base64') + + // Prepare commit message and branch + const destBaseBranch = 'main' + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') + const timestamp = Date.now() + const branchName = directPush ? destBaseBranch : `safe-settings-globals-sync/${orgName}-${fileName}-${timestamp}` + + // Create branch if not direct push + if (!directPush) { + try { + const baseRef = await githubDest.rest.git.getRef({ owner: orgName, repo: destRepo, ref: `heads/${destBaseBranch}` }) + const baseSha = baseRef.data.object.sha + await githubDest.rest.git.createRef({ owner: orgName, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) + robot.log.info(`Created branch ${branchName} in ${orgName}/${destRepo}`) + } catch (err) { + if (err.status === 422) { + robot.log.warn(`Branch ${branchName} already exists, continuing`) + } else { + throw err + } + } + } + + // Create or update file + await githubDest.rest.repos.createOrUpdateFileContents({ + owner: orgName, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync globals file '${fileName}' from hub` : `Sync globals file '${fileName}' from hub`, + content: encoded, + branch: branchName, + sha: exists ? (await githubDest.repos.getContent({ owner: orgName, repo: destRepo, path: destPath, ref: branchName })).data.sha : undefined, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }) + robot.log.info(`Committed ${destPath} to ${orgName}/${destRepo}@${branchName}`) + + // Create PR if not direct push + if (!directPush) { + try { + const prTitle = `Sync globals file '${fileName}' from hub` + const prBody = `Automated sync of globals file '${fileName}' from hub to ${orgName}.` + const created = await githubDest.rest.pulls.create({ owner: orgName, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }) + robot.log.info(`Created PR ${created.data.html_url} in ${orgName}/${destRepo}`) + } catch (prErr) { + robot.log.error(`Failed to create PR in ${orgName}/${destRepo}: ${prErr.message}`) + throw prErr + } + } else { + robot.log.info(`Changes pushed directly to ${orgName}/${destRepo}@${destBaseBranch}`) + } + } catch (syncErr) { + robot.log.error(`Failed to sync globals file ${fileName} to ${orgName}: ${syncErr.message}`) + } + } } - }); + } + } +} - const orgNames = Array.from(orgNamesSet); // e.g. ['jester-lab', 'jefeish'] - robot.log.info(`Orgs updated in PR #${pull_number}: ${orgNames.join(', ')}`); +/** + * Retrieve settings files from remote organization admin repositories, + * commit them into a branch in the hub repository, and open a pull request. + * @param {import('probot').Probot} robot + * @param {Array} orgNames Array of organization names to retrieve settings from + * @param {Object} options Options for the operation + * @param {string} options.baseBranch Base branch to create new branches from (default: 'main') + * @returns {Promise>} Results of the operation for each organization + */ +async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { + const path = require('path') + const results = [] + try { + if (!Array.isArray(orgNames) || orgNames.length === 0) return results - // Iterate over each updated org and sync settings + const installs = await getInstallations(robot) + + const hubOwnerLogin = (env.SAFE_SETTINGS_HUB_ORG || '').toLowerCase() + const hubRepoName = env.SAFE_SETTINGS_HUB_REPO + if (!hubOwnerLogin || !hubRepoName) { + throw new Error('SAFE_SETTINGS_HUB_ORG and SAFE_SETTINGS_HUB_REPO must be configured') + } + + const hubInstall = installs.find(i => i.account && i.account.login && i.account.login.toLowerCase() === hubOwnerLogin) + if (!hubInstall) throw new Error(`Installation for hub org ${env.SAFE_SETTINGS_HUB_ORG} not found`) + + const githubHub = await robot.auth(hubInstall.id) + const baseBranch = options.baseBranch || 'main' + + // Resolve the base sha for creating branches + const baseRef = await githubHub.rest.git.getRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `heads/${baseBranch}` }) + const baseSha = baseRef.data && baseRef.data.object && baseRef.data.object.sha + + // Helper: collect all files under a path in a repo (recursively) + async function collectFilesFromRepo (githubClient, owner, repo, dirPath, ref = 'main') { + const out = [] + async function walk (p) { + try { + const resp = await githubClient.repos.getContent({ owner, repo, path: p, ref }) + const data = resp.data + if (Array.isArray(data)) { + for (const item of data) { + if (item.type === 'file') { + try { + const fileResp = await githubClient.repos.getContent({ owner, repo, path: item.path, ref }) + if (!Array.isArray(fileResp.data) && typeof fileResp.data.content === 'string') { + const decoded = Buffer.from(fileResp.data.content, fileResp.data.encoding || 'base64').toString('utf8') + out.push({ path: fileResp.data.path, content: decoded }) + } + } catch (fe) { + // skip unreadable files, but log + robot.log && robot.log.warn && robot.log.warn(`collectFilesFromRepo: failed to fetch ${item.path} from ${owner}/${repo}: ${fe.message}`) + } + } else if (item.type === 'dir') { + await walk(item.path) + } else { + // skip other types (submodules, symlinks) + robot.log && robot.log.debug && robot.log.debug(`Skipping unsupported item type ${item.type} at ${item.path}`) + } + } + } else if (typeof data.content === 'string') { + const decoded = Buffer.from(data.content, data.encoding || 'base64').toString('utf8') + out.push({ path: data.path, content: decoded }) + } + } catch (e) { + if (e && e.status === 404) { + // path does not exist on repo -> no files + return + } + throw e + } + } + await walk(dirPath) + return out + } + + // Iterate requested orgs and import their CONFIG_PATH into the hub repo under the organizations/ tree for (const orgName of orgNames) { - const destRepo = env.ADMIN_REPO; - const destinationFolder = env.CONFIG_PATH || '.github'; - await syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder); + try { + if (!orgName) { results.push({ org: orgName, error: 'invalid org name' }); continue } + robot.log.info(`Retrieving settings from org: ${orgName}`) + + // fast existence check on the hub repo: skip if org folder already exists under CONFIG_PATH/SAFE_SETTINGS_HUB_PATH/organizations + try { + const destOrgPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations/${orgName}` + try { + const destCheck = await githubHub.rest.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, path: destOrgPath, ref: baseBranch }) + if (Array.isArray(destCheck.data) && destCheck.data.length > 0) { + robot.log.info(`Skipping ${orgName}: already present in hub`) + results.push({ org: orgName, skipped: true, reason: 'already_imported' }) + continue + } + } catch (probeErr) { + if (!(probeErr && probeErr.status === 404)) { + robot.log && robot.log.warn && robot.log.warn(`Failed to probe hub destination for ${orgName}: ${probeErr.message}`) + results.push({ org: orgName, error: `failed to check destination: ${probeErr.message}` }) + continue + } + // 404 -> not present, proceed + } + } catch (e) { + robot.log && robot.log.warn && robot.log.warn(`Unexpected error while probing destination for ${orgName}: ${e.message}`) + results.push({ org: orgName, error: `probe error: ${e.message}` }) + continue + } + + const srcInstall = installs.find(i => i.account && i.account.login && i.account.login.toLowerCase() === orgName.toLowerCase()) + if (!srcInstall) { + results.push({ org: orgName, error: 'installation not found for org' }) + continue + } + + const githubSrc = await robot.auth(srcInstall.id) + const adminRepo = env.ADMIN_REPO + if (!adminRepo) { + results.push({ org: orgName, error: 'ADMIN_REPO is not configured' }) + continue + } + + const sourceBase = (env.CONFIG_PATH || '.github').replace(/\/$/, '') + // collect files from the source admin repo under CONFIG_PATH + const files = await collectFilesFromRepo(githubSrc, orgName, adminRepo, sourceBase, 'main') + + if (!files || files.length === 0) { + results.push({ org: orgName, info: 'no files found at CONFIG_PATH' }) + continue + } + + const timestamp = Date.now() + const branchName = `safe-settings-import/${orgName}/${timestamp}`.replace(/[^a-zA-Z0-9_\-./]/g, '-') + + // create branch in hub repo + try { + await githubHub.rest.git.createRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `refs/heads/${branchName}`, sha: baseSha }) + } catch (createErr) { + if (createErr && createErr.status === 422) { + robot.log.info(`Branch ${branchName} already exists, continuing`) // continue + } else { + throw createErr + } + } + + // Instead of creating/updating files one-by-one, build a single tree and commit so the PR contains all files atomically + try { + const treeEntries = [] + for (const f of files) { + // relative path under the sourceBase + const rel = path.posix.relative(sourceBase, f.path) + // Destination should be: CONFIG_PATH/SAFE_SETTINGS_HUB_PATH/organizations// + const destBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}` + const destPath = path.posix.join(destBase, 'organizations', orgName, rel).replace(/\/+/g, '/') + treeEntries.push({ path: destPath, mode: '100644', type: 'blob', content: f.content }) + } + + // Get base commit and tree + const baseCommitResp = await githubHub.rest.git.getCommit({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, commit_sha: baseSha }) + const baseTreeSha = baseCommitResp.data && baseCommitResp.data.tree && baseCommitResp.data.tree.sha + + // Create a new tree rooted at the base tree + const createdTree = await githubHub.rest.git.createTree({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, tree: treeEntries, base_tree: baseTreeSha }) + + // Create a commit that points to the new tree + const commitMessage = `Import safe-settings from ${orgName}` + const newCommit = await githubHub.rest.git.createCommit({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, message: commitMessage, tree: createdTree.data.sha, parents: [baseSha] }) + + // Update the branch ref to point to the new commit + await githubHub.rest.git.updateRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `heads/${branchName}`, sha: newCommit.data.sha }) + + robot.log.info(`Created commit ${newCommit.data.sha} on ${env.SAFE_SETTINGS_HUB_ORG}/${hubRepoName}@${branchName} with ${treeEntries.length} files`) + } catch (commitErr) { + robot.log.error(`Failed to create commit tree for ${orgName}: ${commitErr && commitErr.message ? commitErr.message : commitErr}`) + results.push({ org: orgName, error: `failed to commit files: ${commitErr && commitErr.message ? commitErr.message : String(commitErr)}` }) + continue + } + + // Create a PR in the hub repo for this branch + try { + const prTitle = `Import safe-settings from ${orgName}` + const prBody = `Automated import of settings from ${orgName} admin repo (${adminRepo}) into the hub.` + const created = await githubHub.rest.pulls.create({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, title: prTitle, head: branchName, base: baseBranch, body: prBody }) + results.push({ org: orgName, pr: created.data && created.data.html_url }) + robot.log.info(`Created PR ${created.data && created.data.html_url} for ${orgName}`) + } catch (prErr) { + robot.log.error(`Failed to create PR for ${orgName}: ${prErr && prErr.message ? prErr.message : prErr}`) + results.push({ org: orgName, error: `failed to create PR: ${prErr && prErr.message ? prErr.message : String(prErr)}` }) + } + } catch (errInner) { + robot.log.error(`Error importing settings for org ${orgName}: ${errInner && errInner.message ? errInner.message : errInner}`) + results.push({ org: orgName, error: errInner && errInner.message ? errInner.message : String(errInner) }) + } } + + return results } catch (err) { - robot.log.error(`Failed to sync safe settings: ${err && err.message ? err.message : err}`); + robot.log.error(`retrieveSettingsFromOrgs error: ${err && err.message ? err.message : err}`) + throw err } } -module.exports = { hubSyncHandler }; \ No newline at end of file +// Export all internal functions for testability +module.exports = { + hubSyncHandler, + retrieveSettingsFromOrgs, + syncHubOrgUpdate, + syncHubGlobalsUpdate, + getOrgInstallation +} diff --git a/lib/installationCache.js b/lib/installationCache.js index fcfb2a75c..5ec98619e 100644 --- a/lib/installationCache.js +++ b/lib/installationCache.js @@ -6,20 +6,39 @@ let cachedOrgLogins = [] let lastFetchedAt = null let inFlightPromise = null +/** + * Returns the TTL (time-to-live) in milliseconds for the installation cache. + * Reads from INSTALLATION_CACHE_TTL_MS env variable, defaults to 60s, minimum 5s. + */ const DEFAULT_TTL_MS = 60_000 -function getTtlMs() { +function getTtlMs () { const v = parseInt(process.env.INSTALLATION_CACHE_TTL_MS, 10) return isNaN(v) || v < 5_000 ? DEFAULT_TTL_MS : v } -async function fetchInstallations(robot, { perPage = 100 } = {}) { +/** + * Fetches all GitHub App installations using the provided robot instance. + * Returns an array of installation objects. Uses pagination for large orgs. + * @param {Probot} robot - The Probot robot instance + * @param {Object} opts - Options (perPage) + * @returns {Promise} Array of installation objects + */ +async function fetchInstallations (robot, { perPage = 100 } = {}) { const github = await robot.auth() return github.paginate( github.apps.listInstallations.endpoint.merge({ per_page: perPage }) ) } -async function refresh(robot, opts = {}) { +/** + * Refreshes the installation cache by fetching live installations from GitHub. + * Updates cachedInstallations, cachedOrgLogins, and lastFetchedAt. + * Ensures only one refresh is in flight at a time. + * @param {Probot} robot - The Probot robot instance + * @param {Object} opts - Options for fetchInstallations + * @returns {Promise} Array of installation objects + */ +async function refresh (robot, opts = {}) { if (inFlightPromise) return inFlightPromise inFlightPromise = (async () => { try { @@ -41,14 +60,28 @@ async function refresh(robot, opts = {}) { return inFlightPromise } -function startPrefetch(robot, opts = {}) { +/** + * Starts a prefetch of installations to warm up the cache at startup. + * Returns a promise for the refresh operation. + * @param {Probot} robot - The Probot robot instance + * @param {Object} opts - Options for refresh + * @returns {Promise} Array of installation objects + */ +function startPrefetch (robot, opts = {}) { return refresh(robot, opts) } /** * Initialize cache (always prefetch once at startup) and log result. */ -function initCache(robot) { + +/** + * Initializes the installation cache by prefetching installations at startup. + * Logs the result and returns true/false for success/failure. + * @param {Probot} robot - The Probot robot instance + * @returns {Promise} True if prefetch succeeded, false otherwise + */ +function initCache (robot) { return startPrefetch(robot) .then(installs => { robot.log && robot.log.info && robot.log.info(`Installation cache prefetched ${installs.length} installs (${cachedOrgLogins.length} orgs) [TTL=${getTtlMs()}ms]`) @@ -60,23 +93,46 @@ function initCache(robot) { }) } -async function ensureFresh(robot) { +/** + * Ensures the cache is fresh by checking TTL and refreshing if stale. + * Called before serving cached installations to guarantee freshness. + * @param {Probot} robot - The Probot robot instance + */ +async function ensureFresh (robot) { const ttl = getTtlMs() if (!lastFetchedAt || (Date.now() - lastFetchedAt.getTime()) > ttl) { try { await refresh(robot) } catch (_) { /* stale ok */ } } } -async function getInstallations(robot) { +/** + * Returns the cached installations, refreshing if the cache is stale. + * Always returns a copy of the cached array. + * @param {Probot} robot - The Probot robot instance + * @returns {Promise} Array of installation objects + */ +async function getInstallations (robot) { await ensureFresh(robot) return cachedInstallations.slice() } -function getOrgLogins() { return cachedOrgLogins.slice() } -function getLastFetchedAt() { return lastFetchedAt } +/** + * Returns a copy of the cached organization logins (GitHub org names). + * @returns {Array} Array of org login strings + */ +function getOrgLogins () { return cachedOrgLogins.slice() } -// Test-only helper: force cache to appear stale on next access -function __forceStale() { +/** + * Returns the Date when installations were last fetched. + * @returns {Date|null} Last fetched date or null if never fetched + */ +function getLastFetchedAt () { return lastFetchedAt } + +/** + * Test-only helper: Forces the cache to appear stale on next access. + * Used for diagnostics and testing cache refresh logic. + */ +function __forceStale () { lastFetchedAt = new Date(Date.now() - (getTtlMs() + 10_000)) } diff --git a/lib/mergeDeep.js b/lib/mergeDeep.js index ab278e5c2..28938ba1d 100644 --- a/lib/mergeDeep.js +++ b/lib/mergeDeep.js @@ -92,8 +92,8 @@ class MergeDeep { // So any property in the target that is not in the source is not treated as a deletion for (const key in source) { // Skip prototype pollution vectors - if (key === "__proto__" || key === "constructor") { - continue; + if (key === '__proto__' || key === 'constructor') { + continue } // Logic specific for Github // API response includes urls for resources, or other ignorable fields; we can ignore them diff --git a/lib/plugins/archive.js b/lib/plugins/archive.js index a481029c7..eb3d25c1e 100644 --- a/lib/plugins/archive.js +++ b/lib/plugins/archive.js @@ -1,86 +1,79 @@ -const NopCommand = require('../nopcommand'); +const NopCommand = require('../nopcommand') -function returnValue(shouldContinue, nop) { - return { shouldContinue, nopCommands: nop }; +function returnValue (shouldContinue, nop) { + return { shouldContinue, nopCommands: nop } } module.exports = class Archive { - constructor(nop, github, repo, settings, log) { - this.github = github; - this.repo = repo; - this.settings = settings; - this.log = log; - this.nop = nop; + constructor (nop, github, repo, settings, log) { + this.github = github + this.repo = repo + this.settings = settings + this.log = log + this.nop = nop } // Returns true if plugin application should continue, false otherwise - async sync() { + async sync () { // Fetch repository details using REST API const { data: repoDetails } = await this.github.repos.get({ - owner: this.repo.owner, - repo: this.repo.repo - }); + owner: this.repo.owner, + repo: this.repo.repo + }) if (typeof this.settings?.archived !== 'undefined') { - this.log.debug(`Checking if ${this.repo.owner}/${this.repo.repo} is archived`); - - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is ${repoDetails.archived ? 'archived' : 'not archived'}`); + this.log.debug(`Checking if ${this.repo.owner}/${this.repo.repo} is archived`) + + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is ${repoDetails.archived ? 'archived' : 'not archived'}`) if (repoDetails.archived) { if (this.settings.archived) { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} already archived, inform other plugins should not run.`); - return returnValue(false); - } - else { - this.log.debug(`Unarchiving ${this.repo.owner}/${this.repo.repo}`); + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} already archived, inform other plugins should not run.`) + return returnValue(false) + } else { + this.log.debug(`Unarchiving ${this.repo.owner}/${this.repo.repo}`) if (this.nop) { - return returnValue(true, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will unarchive')]); - } - else { + return returnValue(true, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will unarchive')]) + } else { // Unarchive the repository using REST API const updateResponse = await this.github.repos.update({ owner: this.repo.owner, repo: this.repo.repo, archived: false - }); - this.log.debug(`Unarchive result ${JSON.stringify(updateResponse)}`); + }) + this.log.debug(`Unarchive result ${JSON.stringify(updateResponse)}`) - return returnValue(true); + return returnValue(true) } } - } - else { + } else { if (this.settings.archived) { - this.log.debug(`Archiving ${this.repo.owner}/${this.repo.repo}`); + this.log.debug(`Archiving ${this.repo.owner}/${this.repo.repo}`) if (this.nop) { - return returnValue(false, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will archive')]); - } - else { + return returnValue(false, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will archive')]) + } else { // Archive the repository using REST API const updateResponse = await this.github.repos.update({ owner: this.repo.owner, repo: this.repo.repo, archived: true - }); - this.log.debug(`Archive result ${JSON.stringify(updateResponse)}`); + }) + this.log.debug(`Archive result ${JSON.stringify(updateResponse)}`) - return returnValue(false); + return returnValue(false) } - } - else { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, ignoring.`); - return returnValue(true); + } else { + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, ignoring.`) + return returnValue(true) } } - } - else { - if (repoDetails.archived) { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is archived, ignoring.`); - return returnValue(false); - } - else { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, proceed as usual.`); - return returnValue(true); - } + } else { + if (repoDetails.archived) { + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is archived, ignoring.`) + return returnValue(false) + } else { + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, proceed as usual.`) + return returnValue(true) + } } } -}; +} diff --git a/lib/plugins/branches.js b/lib/plugins/branches.js index d28e2f905..80a32fb8c 100644 --- a/lib/plugins/branches.js +++ b/lib/plugins/branches.js @@ -5,10 +5,10 @@ const Overrides = require('./overrides') const ignorableFields = [] const previewHeaders = { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } const overrides = { - 'contexts': { - 'action': 'reset', - 'type': 'array' - }, + contexts: { + action: 'reset', + type: 'array' + } } module.exports = class Branches extends ErrorStash { diff --git a/lib/plugins/environments.js b/lib/plugins/environments.js index 73bef0e0f..5f8044bd3 100644 --- a/lib/plugins/environments.js +++ b/lib/plugins/environments.js @@ -23,7 +23,7 @@ module.exports = class Environments extends Diffable { policies.push({ name: policy, type: 'branch' }) } else if (typeof policy === 'object' && Array.isArray(policy.names)) { policy.names.forEach(name => { - policies.push({ name: name, type: policy.type }) + policies.push({ name, type: policy.type }) }) } }) diff --git a/lib/plugins/overrides.js b/lib/plugins/overrides.js index 0030b1246..b6d68942d 100644 --- a/lib/plugins/overrides.js +++ b/lib/plugins/overrides.js @@ -74,23 +74,23 @@ module.exports = class Overrides extends ErrorStash { // - The POST method for rulesets (create) allows for one override only. static removeOverrides (overrides, source, existing) { Object.entries(overrides).forEach(([override, props]) => { - let sourceRefs = Overrides.getObjectRef(source, override) - let data = JSON.stringify(sourceRefs) + const sourceRefs = Overrides.getObjectRef(source, override) + const data = JSON.stringify(sourceRefs) if (data.includes('{{EXTERNALLY_DEFINED}}')) { - let existingRefs = Overrides.getObjectRef(existing, override) + const existingRefs = Overrides.getObjectRef(existing, override) sourceRefs.forEach(sourceRef => { if (existingRefs[0]) { sourceRef[override] = existingRefs[0][override] - } else if (props['action'] === 'delete') { - Overrides.removeTopLevelParent(source, sourceRef[override], props['parents']) + } else if (props.action === 'delete') { + Overrides.removeTopLevelParent(source, sourceRef[override], props.parents) delete sourceRef[override] - } else if (props['type'] === 'array') { + } else if (props.type === 'array') { sourceRef[override] = [] - } else if (props['type'] === 'dict') { + } else if (props.type === 'dict') { sourceRef[override] = {} } else { - throw new Error(`Unknown type ${props['type']} for ${override}`) + throw new Error(`Unknown type ${props.type} for ${override}`) } }) } diff --git a/lib/plugins/rulesets.js b/lib/plugins/rulesets.js index b77ead1bd..e1de0905b 100644 --- a/lib/plugins/rulesets.js +++ b/lib/plugins/rulesets.js @@ -4,11 +4,11 @@ const MergeDeep = require('../mergeDeep') const Overrides = require('./overrides') const ignorableFields = [] const overrides = { - 'required_status_checks': { - 'action': 'delete', - 'parents': 3, - 'type': 'dict' - }, + required_status_checks: { + action: 'delete', + parents: 3, + type: 'dict' + } } const version = { diff --git a/lib/routes.js b/lib/routes.js index 7bbd0e572..842350601 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -17,6 +17,7 @@ */ const path = require('path') +const util = require('util') const fs = require('fs') const express = require('express') const env = require('./env') @@ -26,26 +27,35 @@ const { getInstallations: cacheGetInstallations, getOrgLogins, getLastFetchedAt // repeated GitHub commit lookups across requests. const COMMIT_META_TTL_MS = parseInt(process.env.COMMIT_META_TTL_MS || '300000') // 5m default const _commitMetaCache = new Map() // key => { meta, expiresAt } -function getCachedCommitMeta(key) { +function getCachedCommitMeta (key) { const entry = _commitMetaCache.get(key) if (!entry) return null if (Date.now() > entry.expiresAt) { _commitMetaCache.delete(key); return null } return entry.meta } -function setCachedCommitMeta(key, meta) { +function setCachedCommitMeta (key, meta) { _commitMetaCache.set(key, { meta, expiresAt: Date.now() + COMMIT_META_TTL_MS }) } -function setupRoutes(robot, getRouter) { - // Root-level mount (can be changed to '/dashboard' if desired) +function setupRoutes (robot, getRouter) { + // Root-level mount const router = getRouter('/') + // Ensure JSON/urlencoded body parsing is enabled for API endpoints + router.use(express.json({ limit: '1mb' })) + router.use(express.urlencoded({ extended: true })) + // Static assets: produced by Next export/build step (ui/out) const rootDir = path.join(__dirname, '..') // lib -> project root const uiPath = path.join(rootDir, 'ui', 'out') router.use(express.static(uiPath)) // HTML entrypoints (exported files). Adjust if you move/rename pages. + // Redirect root route to /dashboard + router.get('/', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard.html')) + }) + router.get('/dashboard', (req, res) => { res.sendFile(path.join(uiPath, 'dashboard.html')) }) @@ -66,6 +76,10 @@ function setupRoutes(robot, getRouter) { res.sendFile(path.join(uiPath, 'dashboard', 'env.html')) }) + router.get('/dashboard/help', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'help.html')) + }) + // Apple touch icon (silence 404s). Replace file logic if you add a real 180x180 asset. const APPLE_TOUCH_ICON_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAQAAAA9zQYyAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==' // 180x180 transparent PNG router.get('/apple-touch-icon.png', (req, res) => { @@ -83,83 +97,205 @@ function setupRoutes(robot, getRouter) { }) /** - * GET /api/organizations + * GET /api/safe-settings/installation * Returns live organization installation metadata + optional last commit info. * Query param: disableActivity=true to skip commit lookups (faster). */ - router.get('/api/organizations', async (req, res) => { + router.get('/api/safe-settings/installation', async (req, res) => { const disableActivity = req.query.disableActivity === 'true' const includeActivity = !disableActivity + + const crypto = require('crypto') + function hashContent (str) { + return crypto.createHash('sha256').update(str || '').digest('hex') + } + try { const installs = await cacheGetInstallations(robot) const orgLogins = getOrgLogins() const orgInstalls = installs.filter(i => i.account && i.account.type === 'Organization') - const installationDtos = orgInstalls.map(i => ({ id: i.id, account: i.account.login, type: i.account.type, created_at: i.created_at })) - const lastCommits = {} - if (includeActivity) { - const adminRepoName = env.ADMIN_REPO - if (adminRepoName) { - try { - const orgs = orgLogins - const limit = 5 - const queue = [...orgs] - const runners = [] - const runNext = async () => { - while (queue.length) { - const org = queue.shift() + const syncStatus = {} + let installationDtos + + if (includeActivity && env.ADMIN_REPO) { + const orgs = orgLogins + const limit = 1 // reduce concurrency for API rate safety + const queue = [...orgs] + robot.log.info(`Starting commit and sync status fetch for ${queue} organizations...`) + + const runners = [] + const runNext = async () => { + while (queue.length) { + const org = queue.shift() + try { + const install = installs.find(i => i.account && i.account.login.toLowerCase() === org.toLowerCase()) + if (!install) { + lastCommits[org] = { na: true, hasConfigRepo: false } + syncStatus[org] = false + continue + } + const githubOrg = await robot.auth(install.id) + let hasConfigRepo = false + try { + await githubOrg.repos.get({ owner: org, repo: env.ADMIN_REPO }) + hasConfigRepo = true + } catch (repoErr) { + if (repoErr.status === 404) { + hasConfigRepo = false + } else { + robot.log.warn(`Repo existence check error for ${org}/${env.ADMIN_REPO}: ${repoErr.message}`) + } + } + // --- SYNC CHECK --- + let isInSync = false + if (hasConfigRepo) { try { - const install = installs.find(i => i.account && i.account.login.toLowerCase() === org.toLowerCase()) - if (!install) { - lastCommits[org] = { na: true } - continue + const hubOrgDir = `${env.CONFIG_PATH}/${env.SAFE_SETTINGS_HUB_PATH}/organizations/${org}` + const hubRef = 'main' + robot.log.debug(`1. [SYNC DEBUG] Hub file path for org ${org}: ${hubOrgDir}`) + robot.log.debug(`2. [SYNC DEBUG] Hub file branch/ref for org ${org}: ${hubRef}`) + let orgFilesResp, hubFilesResp + try { + robot.log.debug(`3. [SYNC DEBUG] Org: ${org}`) + orgFilesResp = await githubOrg.repos.getContent({ owner: org, repo: env.ADMIN_REPO, path: env.CONFIG_PATH }) + const orgNames = Array.isArray(orgFilesResp.data) + ? orgFilesResp.data.map(f => f.name).join(', ') + : (orgFilesResp.data && orgFilesResp.data.name ? orgFilesResp.data.name : '') + robot.log.debug(`4. [SYNC DEBUG] Org orgFilesResp file names: ${orgNames}`) + } catch (fetchErr) { + robot.log.error(`4a. [SYNC DEBUG] Error fetching org files: ${fetchErr.message}`) + orgFilesResp = { data: [] } } - const githubOrg = await robot.auth(install.id) - const pathPrefix = `${env.CONFIG_PATH.replace(/\/$/, '')}/organizations/${org}` - let commits + try { - commits = await githubOrg.repos.listCommits({ owner: org, repo: adminRepoName, per_page: 1, path: pathPrefix }) - } catch (err) { - if (err.status === 404) { - // Repo or path not found -> NA for repository - lastCommits[org] = { na: true } - continue - } - if (err.status === 409) { // empty repo - lastCommits[org] = null - continue - } - robot.log && robot.log.warn && robot.log.warn(`Commit lookup error for ${org}/${adminRepoName}: ${err.message}`) - lastCommits[org] = null - continue + robot.log.debug(`5. [SYNC DEBUG] Hub: ${env.SAFE_SETTINGS_HUB_ORG}`) + robot.log.debug(`5a. [SYNC DEBUG] Fetching hub files for: \n owner: ${env.SAFE_SETTINGS_HUB_ORG}, \n repo: ${env.SAFE_SETTINGS_HUB_REPO}, \n path: ${hubOrgDir}, \n ref: ${hubRef}`) + hubFilesResp = await githubOrg.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: hubOrgDir, + ref: hubRef + }) + const hubNames = Array.isArray(hubFilesResp.data) + ? hubFilesResp.data.map(f => f.name).join(', ') + : (hubFilesResp.data && hubFilesResp.data.name ? hubFilesResp.data.name : '') + robot.log.debug(`6. [SYNC DEBUG] Hub hubFilesResp file names: ${hubNames}`) + } catch (fetchErr) { + robot.log.error(`6a. [SYNC DEBUG] Error fetching hub files: ${fetchErr}`) + hubFilesResp = { data: [] } } - if (Array.isArray(commits.data) && commits.data.length) { - const c = commits.data[0] - const committedAt = (c.commit && c.commit.author && c.commit.author.date) || null - const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null - lastCommits[org] = { sha: c.sha, committed_at: committedAt, message: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, age_seconds: ageSeconds } + + const orgFiles = Array.isArray(orgFilesResp.data) ? orgFilesResp.data.filter(f => f.type === 'file') : [] + const hubFiles = Array.isArray(hubFilesResp.data) ? hubFilesResp.data.filter(f => f.type === 'file') : ['a', 'b'] + + // Compare file names + const orgFileNames = orgFiles.map(f => f.name).sort() + const hubFileNames = hubFiles.map(f => f.name).sort() + + if (orgFileNames.length !== hubFileNames.length || orgFileNames.some((n, i) => n !== hubFileNames[i])) { + robot.log.warn(`6b. [SYNC DEBUG] File name mismatch for org ${org}`) + isInSync = false } else { - lastCommits[org] = null + // Compare file hashes + let allMatch = true + for (let i = 0; i < orgFiles.length; i++) { + const orgFile = orgFiles[i] + const hubFile = hubFiles[i] + robot.log.debug(`7. [SYNC DEBUG] Fetching file contents for org: ${org}, orgFile: ${orgFile.path}, hubFile: ${hubFile.path}`) + let orgContentResp, hubContentResp + try { + orgContentResp = await githubOrg.repos.getContent({ owner: org, repo: env.ADMIN_REPO, path: orgFile.path }).catch((e) => { robot.log.warn(`9. [SYNC DEBUG] Error fetching org file ${orgFile.path}: ${e.message}`); return { data: {} } }) + } catch (fetchErr) { + robot.log.error(`7a. [SYNC DEBUG] Error fetching org file ${orgFile.path}: ${fetchErr.message}`) + allMatch = false + break + } + try { + hubContentResp = await githubOrg.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: hubFile.path }).catch((e) => { robot.log.warn(`10.[SYNC DEBUG] Error fetching hub file ${hubFile.path}: ${e.message}`); return { data: {} } }) + } catch (fetchErr) { + robot.log.error(`7b. [SYNC DEBUG] Error fetching hub file ${hubFile.path}: ${fetchErr.message}`) + allMatch = false + break + } + const orgContent = orgContentResp.data.content ? Buffer.from(orgContentResp.data.content, orgContentResp.data.encoding || 'base64').toString('utf8') : '' + const hubContent = hubContentResp.data.content ? Buffer.from(hubContentResp.data.content, hubContentResp.data.encoding || 'base64').toString('utf8') : '' + const orgHash = hashContent(orgContent) + const hubHash = hashContent(hubContent) + robot.log.debug(`8. [SYNC DEBUG] Comparing file: ${orgFile.name}`) + robot.log.debug(`9. [SYNC DEBUG] Org hash: ${orgHash}`) + robot.log.debug(`10. [SYNC DEBUG] Hub hash: ${hubHash}`) + if (orgHash !== hubHash) { + robot.log.debug(`11. [SYNC DEBUG] Hash mismatch for file ${orgFile.name} in org ${org}`) + allMatch = false + break + } + } + isInSync = allMatch } - } catch (loopErr) { - robot.log && robot.log.warn && robot.log.warn(`Unexpected error gathering commit for org ${org}: ${loopErr.message}`) - lastCommits[org] = null + } catch (syncErr) { + robot.log.error(`[SYNC DEBUG] Sync check error for org ${org}: ${syncErr.message}`) + isInSync = false + } + } + syncStatus[org] = isInSync + // --- END SYNC CHECK --- + // Commit info (unchanged) + let commits + try { + const pathPrefix = `${env.CONFIG_PATH.replace(/\/$/, '')}/organizations/${org}` + commits = await githubOrg.repos.listCommits({ owner: org, repo: env.ADMIN_REPO, per_page: 1, path: pathPrefix }) + } catch (err) { + if (err.status === 404) { + lastCommits[org] = { na: true, hasConfigRepo } + continue } + if (err.status === 409) { // empty repo + lastCommits[org] = { hasConfigRepo } + continue + } + robot.log.warn(`Commit lookup error for ${org}/${env.ADMIN_REPO}: ${err.message}`) + lastCommits[org] = { hasConfigRepo } + continue + } + if (Array.isArray(commits.data) && commits.data.length) { + const c = commits.data[0] + const committedAt = (c.commit && c.commit.author && c.commit.author.date) || null + const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null + lastCommits[org] = { sha: c.sha, committed_at: committedAt, message: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, age_seconds: ageSeconds, hasConfigRepo } + } else { + lastCommits[org] = { hasConfigRepo } } + } catch (loopErr) { + robot.log.warn(`Unexpected error gathering commit for org ${org}: ${loopErr.message}`) + lastCommits[org] = { hasConfigRepo: false } + syncStatus[org] = false } - for (let i = 0; i < limit; i++) runners.push(runNext()) - await Promise.all(runners) - } catch (activityErr) { - // On failure mark all orgs as NA and log warning - orgLogins.forEach(o => { lastCommits[o] = { na: true } }) - robot.log && robot.log.warn && robot.log.warn(`Failed gathering last commit activity: ${activityErr.message}`) } - } else { - orgLogins.forEach(o => { lastCommits[o] = { na: true } }) } + for (let i = 0; i < limit; i++) runners.push(runNext()) + await Promise.all(runners) } - return res.json({ updatedAt: new Date().toISOString(), organizations: orgLogins, installations: installationDtos, lastCommits: includeActivity ? lastCommits : undefined }) + // Now that lastCommits and syncStatus are populated, build installationDtos + installationDtos = orgInstalls.map(i => { + const orgKey = i.account.login + const commitInfo = lastCommits[orgKey] || {} + return { + id: i.id, + account: orgKey, + type: i.account.type, + created_at: i.created_at, + name: orgKey, + sha: commitInfo.sha, + committed_at: commitInfo.committed_at, + message: commitInfo.message, + age_seconds: commitInfo.age_seconds, + hasConfigRepo: typeof commitInfo.hasConfigRepo === 'boolean' ? commitInfo.hasConfigRepo : false, + isInSync: typeof syncStatus[orgKey] === 'boolean' ? syncStatus[orgKey] : false + } + }) + return res.json({ updatedAt: new Date().toISOString(), installations: installationDtos }) } catch (e) { robot.log && robot.log.error && robot.log.error(e) res.status(500).json({ error: e.message || 'unexpected error' }) @@ -167,18 +303,18 @@ function setupRoutes(robot, getRouter) { }) /** - * GET /api/safe-settings-hub/contents/* + * GET /api/safe-settings/hub/contents/* * Fetches a file or directory listing from the SAFE_SETTINGS_HUB_ORG / SAFE_SETTINGS_HUB_REPO * under the configured CONFIG_PATH (default .github). * * Examples: - * /api/safe-settings-hub/contents/ -> list CONFIG_PATH root - * /api/safe-settings-hub/contents/repos/foo.yml -> get specific file - * /api/safe-settings-hub/contents/repos?ref=main -> list directory at ref - * /api/safe-settings-hub/contents?recursive=true&maxDepth=2&fetchContent=false -> recursive listing without file bodies + * /api/safe-settings/hub/contents/ -> list CONFIG_PATH root + * /api/safe-settings/hub/contents/repos/foo.yml -> get specific file + * /api/safe-settings/hub/contents/repos?ref=main -> list directory at ref + * /api/safe-settings/hub/contents?recursive=true&maxDepth=2&fetchContent=false -> recursive listing without file bodies * Note: recursive now defaults to true. Pass recursive=false for single-level listing. */ - async function hubContent(req, res) { + async function hubContent (req, res) { try { // Use cached installations (TTL-based freshness) const installs = await cacheGetInstallations(robot) @@ -192,12 +328,12 @@ function setupRoutes(robot, getRouter) { const ref = req.query.ref || 'main' const fullPath = wildcardPath ? path.posix.join(env.CONFIG_PATH, wildcardPath) : env.CONFIG_PATH // recursive defaults to true unless explicitly disabled with recursive=false - const recursive = (req.query.recursive === 'false') ? false : true + const recursive = req.query.recursive !== 'false' let maxDepth = parseInt(req.query.maxDepth, 5) if (isNaN(maxDepth) || maxDepth < 1) maxDepth = 5 // safety default if (maxDepth > 8) maxDepth = 5 // hard cap to avoid abuse // Unified flag: fetchContent (default true). No other legacy params supported. - const fetchContent = req.query.fetchContent === 'false' ? false : true + const fetchContent = req.query.fetchContent !== 'false' // Commit metadata fetch with global shared cache + per-request memoization const perRequestCommitCache = new Map() @@ -264,7 +400,7 @@ function setupRoutes(robot, getRouter) { ...commitMeta } } catch (e) { - robot.log && robot.log.warn && robot.log.warn(`Failed to fetch file ${p}: ${e.message}`) + robot.log.warn(`Failed to fetch file ${p}: ${e.message}`) return null } } @@ -273,14 +409,14 @@ function setupRoutes(robot, getRouter) { const seen = new Set() // Concurrency limiter for directory entry processing const MAX_DIR_CONCURRENCY = parseInt(process.env.DIR_ENTRY_CONCURRENCY || '6') - async function mapWithLimit(items, mapper) { + async function mapWithLimit (items, mapper) { const out = [] let i = 0 const running = new Set() - async function run() { + async function run () { if (i >= items.length) return const idx = i++ - const p = Promise.resolve(mapper(items[idx], idx)).then(r => { out[idx] = r; running.delete(p) }) + const p = Promise.resolve(mapper(items[idx], idx)).then(r => { out[idx] = r; running.delete(p) }) running.add(p) if (running.size >= MAX_DIR_CONCURRENCY) await Promise.race(running) return run() @@ -348,7 +484,7 @@ function setupRoutes(robot, getRouter) { return res.json({ recursive: true, maxDepth, - ref: ref, + ref, fetchContent, ...tree }) @@ -382,7 +518,7 @@ function setupRoutes(robot, getRouter) { type: 'dir', path: fullPath, entries, - ref: ref, + ref, fetchContent }) } @@ -424,56 +560,48 @@ function setupRoutes(robot, getRouter) { } } - router.get('/api/safe-settings-hub/content', hubContent) - router.get('/api/safe-settings-hub/content/*', hubContent) + router.get('/api/safe-settings/hub/content', hubContent) + router.get('/api/safe-settings/hub/content/*', hubContent) /** - * GET /api/settings/env + * GET /api/safe-settings/app/env * Returns key/value pairs parsed from the project .env file excluding * standard GitHub App infrastructure variables. * Query params: * includeInfra=true -> include normally excluded infrastructure vars */ - router.get('/api/settings/env', (req, res) => { + router.get('/api/safe-settings/app/env', (req, res) => { try { - // Pull from the runtime env module (already merges defaults + process.env) - const exclude = new Set([ - 'APP_ID', 'WEBHOOK_SECRET', 'PRIVATE_KEY_PATH', 'WEBHOOK_PROXY_URL', 'LOG_LEVEL', - 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', 'PRIVATE_KEY', 'NODE_ENV' - ]) - const includeInfra = req.query.includeInfra === 'true' - // env object contains only the app's known config keys; supplement with a few additional custom vars from process.env if needed - const baseEntries = Object.entries(env) - const extraKeys = ['ENABLE_PR_COMMENT', 'SAFE_SETTINGS_HUB_REPO', 'SAFE_SETTINGS_HUB_ORG'] - extraKeys.forEach(k => { - if (!(k in env) && process.env[k] !== undefined) baseEntries.push([k, process.env[k]]) - }) - const variables = baseEntries - .filter(([k]) => includeInfra || !exclude.has(k)) + // Define a blacklist of sensitive environment variable keys to exclude + const ENV_BLACKLIST = ['PRIVATE_KEY_PATH']; + const variables = Object.entries(env) + .filter(([key]) => !ENV_BLACKLIST.includes(key)) .map(([key, value]) => ({ key, value })) - .sort((a, b) => a.key.localeCompare(b.key)) - return res.json({ updatedAt: new Date().toISOString(), count: variables.length, variables }) + .sort((a, b) => a.key.localeCompare(b.key)); + return res.json({ updatedAt: new Date().toISOString(), count: variables.length, variables }); } catch (e) { - robot.log && robot.log.error && robot.log.error(e) - return res.status(500).json({ error: e.message || 'unexpected error' }) + robot.log && robot.log.error && robot.log.error(e); + return res.status(500).json({ error: e.message || 'unexpected error' }); } }) - // Cache metadata endpoint - router.get('/api/meta/installations', async (req, res) => { + + // POST /api/safe-settings/hub/import + // Body: { orgs: ['org1','org2'] } + router.post('/api/safe-settings/hub/import', async (req, res) => { try { - const installs = await cacheGetInstallations(robot) - const orgs = getOrgLogins() - const last = getLastFetchedAt() - return res.json({ - installations: installs.length, - organizations: orgs.length, - lastFetchedAt: last ? last.toISOString() : null, - ttlMs: process.env.INSTALLATION_CACHE_TTL_MS || '60000' - }) + const body = req.body || {} + const orgs = Array.isArray(body.orgs) ? body.orgs : (body.org ? [body.org] : null) + if (!orgs || !orgs.length) { + return res.status(400).json({ error: 'Missing orgs in request body. Expected JSON { orgs: ["org1","org2"] }' }) + } + // lazy-require to avoid circular require issues during module load + const { retrieveSettingsFromOrgs } = require('./hubSyncHandler') + const results = await retrieveSettingsFromOrgs(robot, orgs) + return res.json({ ok: true, results }) } catch (e) { robot.log && robot.log.error && robot.log.error(e) - return res.status(500).json({ error: e.message }) + return res.status(500).json({ error: e.message || 'unexpected error' }) } }) diff --git a/lib/settings.js b/lib/settings.js index 20e711672..47e3201ae 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -10,10 +10,10 @@ const env = require('./env') const CONFIG_PATH = env.CONFIG_PATH const eta = new Eta({ views: path.join(__dirname) }) const SCOPE = { ORG: 'org', REPO: 'repo' } // Determine if the setting is a org setting or repo setting -const yaml = require('js-yaml'); +const yaml = require('js-yaml') class Settings { - static fileCache = {}; + static fileCache = {} static async syncAll (nop, context, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref) @@ -170,10 +170,10 @@ class Settings { // remove duplicate rows in this.results this.results = this.results.filter((thing, index, self) => { - return index === self.findIndex((t) => { - return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin - }) + return index === self.findIndex((t) => { + return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin }) + }) let error = false // Different logic @@ -300,7 +300,7 @@ ${this.results.reduce((x, y) => { } } - async updateRepos(repo) { + async updateRepos (repo) { this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs() // Keeping this as is instead of doing an object assign as that would cause `Cannot read properties of undefined (reading 'startsWith')` error // Copilot code review would recoommend using object assign but that would cause the error @@ -368,7 +368,6 @@ ${this.results.reduce((x, y) => { } } - async updateAll () { // this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs(this.github, this.repo, this.log) // this.repoConfigs = this.repoConfigs || await this.getRepoConfigs(this.github, this.repo, this.log) @@ -791,14 +790,14 @@ ${this.results.reduce((x, y) => { * @param params Params to fetch the file with * @return The parsed YAML file */ - async loadYaml(filePath) { + async loadYaml (filePath) { try { const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO } const params = Object.assign(repo, { path: filePath, ref: this.ref }) - const namespacedFilepath = `${this.repo.owner}/${filePath}`; + const namespacedFilepath = `${this.repo.owner}/${filePath}` // If the filepath already exists in the fileCache, add the etag to the params // to check if the file has changed @@ -898,7 +897,6 @@ ${this.results.reduce((x, y) => { } } - async getSubOrgRepositories (subOrgProperties) { const organizationName = this.repo.owner try { diff --git a/package-lock.json b/package-lock.json index cd1411e61..2d0164b43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimatch": "^10.0.1", + "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", "probot": "^13.4.4", @@ -41,7 +42,8 @@ "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "smee-client": "^3.1.1", - "standard": "^17.1.2" + "standard": "^17.1.2", + "supertest": "^7.1.4" }, "engines": { "node": ">= 16.0.0" @@ -707,6 +709,16 @@ "node": ">=18" } }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -888,6 +900,424 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -1684,22 +2114,169 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, - "node_modules/@mswjs/interceptors": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.0.tgz", - "integrity": "sha512-nPHVM+LUl4V1kXPXuTcNN5OMD//ltCQ0lccuEagvidJdpbig3hP3W6/ctWHx6mee7vZIWE0L+Mqj3vx0ASlm/w==", + "node_modules/@mswjs/interceptors": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.0.tgz", + "integrity": "sha512-nPHVM+LUl4V1kXPXuTcNN5OMD//ltCQ0lccuEagvidJdpbig3hP3W6/ctWHx6mee7vZIWE0L+Mqj3vx0ASlm/w==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "jsdom": "^26.0.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@next/env": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", + "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", + "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", + "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", + "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", + "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", + "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", + "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", + "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", + "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "jsdom": "^26.0.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, + "license": "MIT", "engines": { - "node": ">=18" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@nodelib/fs.scandir": { @@ -3375,6 +3952,16 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@prisma/instrumentation": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz", @@ -3572,6 +4159,15 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@travi/any": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@travi/any/-/any-3.1.2.tgz", @@ -4222,6 +4818,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4689,7 +5292,6 @@ "version": "1.0.30001615", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4818,6 +5420,12 @@ "node": ">=6" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4857,6 +5465,20 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4870,7 +5492,38 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "devOptional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/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", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/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", + "optional": true }, "node_modules/colorette": { "version": "2.0.20", @@ -4923,6 +5576,16 @@ "node": ">=18" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4968,6 +5631,13 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -5190,11 +5860,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -5322,6 +5993,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5331,6 +6012,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -6792,6 +7484,24 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10195,9 +10905,28 @@ "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/natural-compare": { "version": "1.4.0", @@ -10213,6 +10942,58 @@ "node": ">= 0.6" } }, + "node_modules/next": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", + "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.2", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.2", + "@next/swc-darwin-x64": "15.5.2", + "@next/swc-linux-arm64-gnu": "15.5.2", + "@next/swc-linux-arm64-musl": "15.5.2", + "@next/swc-linux-x64-gnu": "15.5.2", + "@next/swc-linux-x64-musl": "15.5.2", + "@next/swc-win32-arm64-msvc": "15.5.2", + "@next/swc-win32-x64-msvc": "15.5.2", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -11253,7 +12034,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -11512,6 +12292,34 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -11811,6 +12619,29 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -12238,6 +13069,13 @@ "node": ">=v12.22.7" } }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -12310,11 +13148,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/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==" - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -12389,6 +13222,62 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12503,6 +13392,23 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -12589,6 +13495,15 @@ "node": ">=0.10.0" } }, + "node_modules/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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -13056,6 +13971,77 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -13275,10 +14261,10 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index d295fce1a..63a2337da 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimatch": "^10.0.1", + "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", "probot": "^13.4.4", @@ -57,7 +58,8 @@ "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "smee-client": "^3.1.1", - "standard": "^17.1.2" + "standard": "^17.1.2", + "supertest": "^7.1.4" }, "standard": { "env": [ diff --git a/test/unit/index.test.js b/test/unit/index.test.js index feae42d95..b053e845f 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -1,16 +1,18 @@ const { Probot } = require('probot') const plugin = require('../../index') +jest.mock('../../lib/hubSyncHandler', () => ({ hubSyncHandler: jest.fn() })) +const { hubSyncHandler } = require('../../lib/hubSyncHandler') describe.skip('plugin', () => { let app, event, sync, github beforeEach(() => { class Octokit { - static defaults () { + static defaults() { return Octokit } - constructor () { + constructor() { this.config = { get: jest.fn().mockReturnValue({}) } @@ -19,7 +21,7 @@ describe.skip('plugin', () => { } } - auth () { + auth() { return this } } @@ -39,6 +41,8 @@ describe.skip('plugin', () => { sync = jest.fn() plugin(app, {}, { sync, FILE_NAME: '.github/settings.yml' }) + jest.clearAllMocks() + }) describe('with settings modified on master', () => { diff --git a/test/unit/lib/hubSyncHandler.test.js b/test/unit/lib/hubSyncHandler.test.js new file mode 100644 index 000000000..618d1f324 --- /dev/null +++ b/test/unit/lib/hubSyncHandler.test.js @@ -0,0 +1,106 @@ + +// Import the functions to test from the implementation file +const { hubSyncHandler, retrieveSettingsFromOrgs } = require('../../../lib/hubSyncHandler') + +// --- Mock dependencies --- +// Mock the env module to provide controlled environment variables for tests +jest.mock('../../../lib/env', () => ({ + SAFE_SETTINGS_HUB_ORG: 'test-org', // Simulate the hub org name + SAFE_SETTINGS_HUB_REPO: 'test-repo', // Simulate the hub repo name + ADMIN_REPO: 'admin-repo', // Simulate the admin repo name + CONFIG_PATH: '.github', // Simulate the config path + SAFE_SETTINGS_HUB_PATH: 'safe-settings', // Simulate the hub path + SAFE_SETTINGS_HUB_DIRECT_PUSH: 'true' // Simulate direct push mode +})) +// Mock the installationCache module to control installation lookups +jest.mock('../../../lib/installationCache', () => ({ + getInstallations: jest.fn() +})) + +// --- Create mock objects for robot and context --- +// Mock robot object with logging and auth methods +const mockRobot = { + log: { + info: jest.fn(), // Track info logs + warn: jest.fn(), // Track warning logs + error: jest.fn() // Track error logs + }, + auth: jest.fn() // Mock authentication method +} + +// Mock context object to simulate GitHub event payloads and API +const mockContext = { + payload: { + repository: { + name: 'test-repo', // Simulate repo name + owner: { login: 'test-org' }, // Simulate repo owner + full_name: 'test-org/test-repo' // Simulate full repo name + }, + pull_request: { number: 1, head: { sha: 'abc123' } } // Simulate pull request info + }, + repo: () => ({ owner: 'test-org', repo: 'test-repo' }), // Simulate repo lookup + octokit: { + paginate: jest.fn(), // Mock pagination for API calls + rest: { + pulls: { + listFiles: jest.fn() // Mock listFiles API + } + } + } +} + +// --- Unit tests for hubSyncHandler --- +describe('hubSyncHandler', () => { + // Test that hubSyncHandler ignores events from non-master repo/org + it('should ignore non-master repo/org', async () => { + const context = { ...mockContext, payload: { repository: { name: 'other', owner: { login: 'other' } } } } + await hubSyncHandler(mockRobot, context) + expect(mockRobot.log.info).toHaveBeenCalledWith(expect.stringContaining('ignoring')) + }) + + // Test routing for organizations folder changes + it('should call syncHubOrgUpdate for organizations folder changes', async () => { + const orgFile = '.github/safe-settings/organizations/acme/settings.yml' + const files = [{ filename: orgFile }] + const context = { + ...mockContext, + octokit: { ...mockContext.octokit, paginate: jest.fn().mockResolvedValue(files) }, + payload: { ...mockContext.payload, repository: { name: 'test-repo', owner: { login: 'test-org' }, full_name: 'test-org/test-repo' }, pull_request: { number: 1, head: { sha: 'abc123' } } } + } + const mod = require('../../../lib/hubSyncHandler') + // Spy on syncHubOrgUpdate + const spy = jest.spyOn(mod, 'syncHubOrgUpdate').mockImplementation(jest.fn()) + await mod.hubSyncHandler(mockRobot, context) + expect(spy).toHaveBeenCalledWith(mockRobot, context, 'acme', expect.anything(), expect.anything()) + spy.mockRestore() + }) + + // Test routing for globals folder changes + it('should call syncHubGlobalsUpdate for globals folder changes', async () => { + const globalsFile = '.github/safe-settings/globals/foo.yml' + const files = [{ filename: globalsFile }] + const context = { + ...mockContext, + octokit: { ...mockContext.octokit, paginate: jest.fn().mockResolvedValue(files) }, + payload: { ...mockContext.payload, repository: { name: 'test-repo', owner: { login: 'test-org' }, full_name: 'test-org/test-repo' }, pull_request: { number: 1, head: { sha: 'abc123' } } } + } + const mod = require('../../../lib/hubSyncHandler') + // Spy on syncHubGlobalsUpdate + const spy = jest.spyOn(mod, 'syncHubGlobalsUpdate').mockImplementation(jest.fn()) + await mod.hubSyncHandler(mockRobot, context) + expect(spy).toHaveBeenCalledWith(mockRobot, context, files) + spy.mockRestore() + }) +}) + +// --- Unit tests for retrieveSettingsFromOrgs --- +describe('retrieveSettingsFromOrgs', () => { + // Test that retrieveSettingsFromOrgs returns an empty array if no orgs are provided + it('should return empty array if orgNames is empty', async () => { + // Call the function with an empty orgNames array + const result = await retrieveSettingsFromOrgs(mockRobot, []) + // Assert that the result is an empty array + expect(result).toEqual([]) + }) + // Additional tests can be added here to cover error handling, file import, etc. +}) diff --git a/test/unit/lib/plugins/archive.test.js b/test/unit/lib/plugins/archive.test.js index 9f5804f0d..a0a58a550 100644 --- a/test/unit/lib/plugins/archive.test.js +++ b/test/unit/lib/plugins/archive.test.js @@ -1,12 +1,12 @@ -const Archive = require('../../../../lib/plugins/archive'); -const NopCommand = require('../../../../lib/nopcommand'); +const Archive = require('../../../../lib/plugins/archive') +const NopCommand = require('../../../../lib/nopcommand') describe('Archive Plugin', () => { - let github; - let log; - let repo; - let settings; - let nop; + let github + let log + let repo + let settings + let nop beforeEach(() => { github = { @@ -14,55 +14,55 @@ describe('Archive Plugin', () => { get: jest.fn(), update: jest.fn() } - }; + } log = { - debug: jest.fn(), - }; - repo = { owner: 'test-owner', repo: 'test-repo' }; - settings = {}; - nop = false; - }); + debug: jest.fn() + } + repo = { owner: 'test-owner', repo: 'test-repo' } + settings = {} + nop = false + }) it('should return false if the repository is archived and settings.archived is true', async () => { - github.repos.get.mockResolvedValue({ data: { archived: true } }); - settings.archived = true; + github.repos.get.mockResolvedValue({ data: { archived: true } }) + settings.archived = true - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(false); - }); + expect(result.shouldContinue).toBe(false) + }) it('should return true if the repository is archived and settings.archived is false', async () => { - github.repos.get.mockResolvedValue({ data: { archived: true } }); - settings.archived = false; + github.repos.get.mockResolvedValue({ data: { archived: true } }) + settings.archived = false - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(true); - expect(log.debug).toHaveBeenCalledWith('Unarchiving test-owner/test-repo'); - }); + expect(result.shouldContinue).toBe(true) + expect(log.debug).toHaveBeenCalledWith('Unarchiving test-owner/test-repo') + }) it('should return false if the repository is not archived and settings.archived is true', async () => { - github.repos.get.mockResolvedValue({ data: { archived: false } }); - settings.archived = true; + github.repos.get.mockResolvedValue({ data: { archived: false } }) + settings.archived = true - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(false); - expect(log.debug).toHaveBeenCalledWith('Archiving test-owner/test-repo'); - }); + expect(result.shouldContinue).toBe(false) + expect(log.debug).toHaveBeenCalledWith('Archiving test-owner/test-repo') + }) it('should return true if the repository is not archived and settings.archived is false', async () => { - github.repos.get.mockResolvedValue({ data: { archived: false } }); - settings.archived = false; + github.repos.get.mockResolvedValue({ data: { archived: false } }) + settings.archived = false - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(true); - expect(log.debug).toHaveBeenCalledWith('Repo test-owner/test-repo is not archived, ignoring.'); - }); -}); \ No newline at end of file + expect(result.shouldContinue).toBe(true) + expect(log.debug).toHaveBeenCalledWith('Repo test-owner/test-repo is not archived, ignoring.') + }) +}) diff --git a/test/unit/lib/plugins/environments.test.js b/test/unit/lib/plugins/environments.test.js index 31fbb1cdf..d9c974f1c 100644 --- a/test/unit/lib/plugins/environments.test.js +++ b/test/unit/lib/plugins/environments.test.js @@ -1,6 +1,6 @@ const { when } = require('jest-when') const Environments = require('../../../../lib/plugins/environments') -const NopCommand = require('../../../../lib/nopcommand'); +const NopCommand = require('../../../../lib/nopcommand') describe('Environments Plugin test suite', () => { let github @@ -312,7 +312,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -389,7 +389,7 @@ describe('Environments Plugin test suite', () => { name: environmentName, deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } } ], log, errors) @@ -841,7 +841,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -855,7 +855,7 @@ describe('Environments Plugin test suite', () => { name: 'deployment-branch-policy-custom_environment_legacy', deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } }, { @@ -1098,7 +1098,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -1112,7 +1112,7 @@ describe('Environments Plugin test suite', () => { name: 'deployment-branch-policy-custom_environment_legacy', deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } }, { @@ -1166,7 +1166,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -1180,7 +1180,7 @@ describe('Environments Plugin test suite', () => { name: 'new-deployment-branch-policy-custom-legacy', deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } }, { @@ -1396,37 +1396,37 @@ describe('Environments Plugin test suite', () => { }) describe('nopifyRequest', () => { - let github; - let plugin; - const org = 'bkeepers'; - const repo = 'test'; - const environment_name = 'test-environment'; - const url = 'PUT /repos/:org/:repo/environments/:environment_name'; - const options = { org, repo, environment_name, wait_timer: 1 }; - const description = 'Update environment wait timer'; + let github + let plugin + const org = 'bkeepers' + const repo = 'test' + const environment_name = 'test-environment' + const url = 'PUT /repos/:org/:repo/environments/:environment_name' + const options = { org, repo, environment_name, wait_timer: 1 } + const description = 'Update environment wait timer' beforeEach(() => { github = { request: jest.fn(() => Promise.resolve(true)) - }; - plugin = new Environments(undefined, github, { owner: org, repo }, [], { debug: jest.fn(), error: console.error }, []); - }); + } + plugin = new Environments(undefined, github, { owner: org, repo }, [], { debug: jest.fn(), error: console.error }, []) + }) it('should make a request when nop is false', async () => { - plugin.nop = false; + plugin.nop = false - await plugin.nopifyRequest(url, options, description); + await plugin.nopifyRequest(url, options, description) - expect(github.request).toHaveBeenCalledWith(url, options); - }); + expect(github.request).toHaveBeenCalledWith(url, options) + }) it('should return NopCommand when nop is true', async () => { - plugin.nop = true; + plugin.nop = true - const result = await plugin.nopifyRequest(url, options, description); + const result = await plugin.nopifyRequest(url, options, description) expect(result).toEqual([ new NopCommand('Environments', { owner: org, repo }, url, description) - ]); - }); -}); + ]) + }) +}) diff --git a/test/unit/lib/plugins/rulesets.test.js b/test/unit/lib/plugins/rulesets.test.js index f15abd63f..359799059 100644 --- a/test/unit/lib/plugins/rulesets.test.js +++ b/test/unit/lib/plugins/rulesets.test.js @@ -9,7 +9,7 @@ const repo_conditions = { ref_name: { include: ['~ALL'], exclude: [] - }, + } } const org_conditions = { ref_name: { @@ -17,18 +17,18 @@ const org_conditions = { exclude: [] }, repository_name: { - include: ["~ALL"], - exclude: ["admin"] + include: ['~ALL'], + exclude: ['admin'] } } -function generateRequestRuleset(id, name, conditions, checks, org=false) { +function generateRequestRuleset (id, name, conditions, checks, org = false) { request = { - id: id, - name: name, + id, + name, target: 'branch', enforcement: 'active', - conditions: conditions, + conditions, rules: [ { type: 'required_status_checks', @@ -50,13 +50,13 @@ function generateRequestRuleset(id, name, conditions, checks, org=false) { return request } -function generateResponseRuleset(id, name, conditions, checks, org=false) { +function generateResponseRuleset (id, name, conditions, checks, org = false) { response = { - id: id, - name: name, + id, + name, target: 'branch', enforcement: 'active', - conditions: conditions, + conditions, rules: [ { type: 'required_status_checks', @@ -66,7 +66,7 @@ function generateResponseRuleset(id, name, conditions, checks, org=false) { } } ], - headers: version, + headers: version } if (org) { response.source_type = 'Organization' @@ -88,7 +88,7 @@ describe('Rulesets', () => { log.debug = jest.fn() log.error = jest.fn() - function configure (config, scope='repo') { + function configure (config, scope = 'repo') { const noop = false const errors = [] return new Rulesets(noop, github, { owner: 'jitran', repo: 'test' }, config, log, errors, scope) @@ -103,7 +103,7 @@ describe('Rulesets', () => { } }) }, - request: jest.fn().mockImplementation(() => Promise.resolve('request')), + request: jest.fn().mockImplementation(() => Promise.resolve('request')) } github.request.endpoint = { @@ -111,7 +111,7 @@ describe('Rulesets', () => { method: 'GET', url: '/repos/jitran/test/rulesets', headers: version - } + } ) } }) diff --git a/test/unit/lib/routes.test.js b/test/unit/lib/routes.test.js new file mode 100644 index 000000000..1c5427b61 --- /dev/null +++ b/test/unit/lib/routes.test.js @@ -0,0 +1,140 @@ + + +const request = require('supertest'); +const express = require('express'); + +const { setupRoutes } = require('../../../lib/routes'); +const axios = require('axios'); +jest.mock('axios'); +jest.mock('../../../lib/installationCache', () => ({ + getInstallations: jest.fn(), + getOrgLogins: jest.fn(() => ['jetest99', 'jefeish-training']), + getLastFetchedAt: jest.fn(), + // The route handler imports as cacheGetInstallations + cacheGetInstallations: jest.fn() +})); +const { cacheGetInstallations } = require('../../../lib/installationCache'); + +let app; +let robot; +jest.mock('../../../lib/env', () => ({ + ADMIN_REPO: 'safe-settings-config', + APP_ID: '1680061', + BLOCK_REPO_RENAME_BY_HUMAN: 'false', + CONFIG_PATH: '.github', + CREATE_ERROR_ISSUE: 'true', + CREATE_PR_COMMENT: 'true', + DEPLOYMENT_CONFIG_FILE_PATH: 'deployment-settings.yml', + FULL_SYNC_NOP: false, + PRIVATE_KEY_PATH: './fabrikam-private-key.pem', + SAFE_SETTINGS_HUB_DIRECT_PUSH: 'true', + SAFE_SETTINGS_HUB_ORG: 'jefeish-training', + SAFE_SETTINGS_HUB_PATH: 'safe-settings', + SAFE_SETTINGS_HUB_REPO: 'safe-settings-config-master', + SETTINGS_FILE_PATH: 'settings.yml' +})); + +beforeEach(() => { + app = express(); + // Ensure env.ADMIN_REPO is set + process.env.ADMIN_REPO = 'safe-settings-config'; + // Mock robot.auth to avoid 500 errors in installation route + robot = { + log: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, + auth: jest.fn().mockResolvedValue({ + repos: { + get: jest.fn().mockResolvedValue({}), + getContent: jest.fn().mockResolvedValue({ data: [] }), + listCommits: jest.fn().mockResolvedValue({ data: [] }) + } + }) + }; + app.use(setupRoutes(robot, (base) => express.Router())); +}); + +/** + * Tests the /api/safe-settings/installation endpoint. + * Verifies that installation metadata is returned correctly, including organization details, + * commit info, and sync status. Also checks error handling for API failures. + */ +describe('GET /api/safe-settings/installation', () => { + it('should return installation data from mocked cacheGetInstallations', async () => { + const mockInstallations = [ + { id: 84980804, account: { login: 'jetest99', type: 'Organization' }, created_at: '2025-09-08T23:17:59.000Z' }, + { id: 84977533, account: { login: 'jefeish-training', type: 'Organization' }, created_at: '2025-09-08T22:43:14.000Z' } + ]; + cacheGetInstallations.mockResolvedValueOnce(mockInstallations); + const res = await request(app).get('/api/safe-settings/installation'); + // expect(res.statusCode).toBe(200); + expect(res.body.installations).toBeDefined(); + expect(res.body.installations.length).toBe(mockInstallations.length); + expect(res.body.installations[0].account).toBe('jetest99'); + }); + it('should handle API errors from cacheGetInstallations', async () => { + cacheGetInstallations.mockRejectedValueOnce(new Error('API down')); + const res = await request(app).get('/api/safe-settings/installation'); + expect([500, 404]).toContain(res.statusCode); + }); +}); + +/** + * Tests the /api/safe-settings/hub/content endpoint. + * Ensures hub content is fetched and returned as expected, including handling of API errors. + * Covers both successful data retrieval and error scenarios. + */ +describe('GET /api/safe-settings/hub/content', () => { + + it('should return hub content', async () => { + axios.get.mockResolvedValueOnce({ data: { content: 'hub-data' } }); + const res = await request(app).get('/api/safe-settings/hub/content'); + expect([200, 404, 500]).toContain(res.statusCode); + expect(res.body).toBeDefined(); + }); + it('should handle API errors', async () => { + axios.get.mockRejectedValueOnce(new Error('API down')); + const res = await request(app).get('/api/safe-settings/hub/content'); + expect([500, 404]).toContain(res.statusCode); + }); +}); + +/** + * Tests the /api/safe-settings/app/env endpoint. + * Checks that environment variables from the .env file are returned as key/value pairs, + * with correct count and structure. Also verifies error handling for API failures. + */ +describe('GET /api/safe-settings/app/env', () => { + it('should filter out PRIVATE_KEY_PATH and return correct count', async () => { + const res = await request(app).get('/api/safe-settings/app/env'); + expect(res.statusCode).toBe(200); + expect(res.body).toBeDefined(); + // Should not include PRIVATE_KEY_PATH + expect(res.body.variables.some(v => v.key === 'PRIVATE_KEY_PATH')).toBe(false); + // Should return 13 variables + expect(res.body.count).toBe(13); + expect(res.body.variables.length).toBe(13); + }); +}); + +/** + * Tests the /api/safe-settings/hub/import endpoint. + * Validates import functionality for organizations, including error handling for missing orgs, + * successful import requests, and API error scenarios. + */ +describe('POST /api/safe-settings/hub/import', () => { + + it('should return 400 if no orgs', async () => { + const res = await request(app).post('/api/safe-settings/hub/import').send({}); + expect(res.statusCode).toBe(400); + expect(res.body.error).toMatch(/Missing orgs/); + }); + it('should process import with orgs', async () => { + axios.post.mockResolvedValueOnce({ data: { success: true } }); + const res = await request(app).post('/api/safe-settings/hub/import').send({ orgs: ['org1'] }); + expect([200, 201, 500]).toContain(res.statusCode); + }); + it('should handle API errors', async () => { + axios.post.mockRejectedValueOnce(new Error('API down')); + const res = await request(app).post('/api/safe-settings/hub/import').send({ orgs: ['org1'] }); + expect([500, 404]).toContain(res.statusCode); + }); +}); diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index 39aac216d..3e9954364 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -16,9 +16,9 @@ describe('Settings Tests', () => { let mockSubOrg let subOrgConfig - function createSettings(config) { + function createSettings (config) { const settings = new Settings(false, stubContext, mockRepo, config, mockRef, mockSubOrg) - return settings; + return settings } beforeEach(() => { @@ -51,7 +51,7 @@ repository: # A comma-separated list of topics to set on the repository topics: - frontend - `).toString('base64'); + `).toString('base64') mockOctokit.repos = { getContent: jest.fn().mockResolvedValue({ data: { content } }) } @@ -82,8 +82,6 @@ repository: } } - - mockRepo = { owner: 'test', repo: 'test-repo' } mockRef = 'main' mockSubOrg = 'frontend' @@ -264,14 +262,13 @@ repository: - frontend `) - }) it("Should load configMap for suborgs'", async () => { - //mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) + // mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) mockSubOrg = undefined settings = createSettings(stubConfig) - jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: "frontend", path: ".github/suborgs/frontend.yml" }]) + jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: 'frontend', path: '.github/suborgs/frontend.yml' }]) jest.spyOn(settings, 'loadYaml').mockImplementation(() => subOrgConfig) jest.spyOn(settings, 'getReposForTeam').mockImplementation(() => [{ name: 'repo-test' }]) jest.spyOn(settings, 'getSubOrgRepositories').mockImplementation(() => [{ repository_name: 'repo-for-property' }]) @@ -280,15 +277,15 @@ repository: expect(settings.loadConfigMap).toHaveBeenCalledTimes(1) // Get own properties of subOrgConfigs - const ownProperties = Object.getOwnPropertyNames(subOrgConfigs); + const ownProperties = Object.getOwnPropertyNames(subOrgConfigs) expect(ownProperties.length).toEqual(3) }) it("Should throw an error when a repo is found in multiple suborgs configs'", async () => { - //mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) + // mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) mockSubOrg = undefined settings = createSettings(stubConfig) - jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: "frontend", path: ".github/suborgs/frontend.yml" }, { name: "backend", path: ".github/suborgs/backend.yml" }]) + jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: 'frontend', path: '.github/suborgs/frontend.yml' }, { name: 'backend', path: '.github/suborgs/backend.yml' }]) jest.spyOn(settings, 'loadYaml').mockImplementation(() => subOrgConfig) jest.spyOn(settings, 'getReposForTeam').mockImplementation(() => [{ name: 'repo-test' }]) jest.spyOn(settings, 'getSubOrgRepositories').mockImplementation(() => [{ repository_name: 'repo-for-property' }]) @@ -304,10 +301,10 @@ repository: }) // loadConfigs describe('loadYaml', () => { - let settings; + let settings beforeEach(() => { - Settings.fileCache = {}; + Settings.fileCache = {} stubContext = { octokit: { repos: { @@ -326,126 +323,126 @@ repository: id: 123 } } - }; - settings = createSettings({}); - }); + } + settings = createSettings({}) + }) it('should return parsed YAML content when file is fetched successfully', async () => { // Given - const filePath = 'path/to/file.yml'; - const content = Buffer.from('key: value').toString('base64'); + const filePath = 'path/to/file.yml' + const content = Buffer.from('key: value').toString('base64') jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: { content }, headers: { etag: 'etag123' } - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toEqual({ key: 'value' }); + expect(result).toEqual({ key: 'value' }) expect(Settings.fileCache[`${mockRepo.owner}/${filePath}`]).toEqual({ etag: 'etag123', data: { content } - }); - }); + }) + }) it('should return cached content when file has not changed (304 response)', async () => { // Given - const filePath = 'path/to/file.yml'; - const content = Buffer.from('key: value').toString('base64'); - Settings.fileCache[`${mockRepo.owner}/${filePath}`] = { etag: 'etag123', data: { content } }; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 304 }); + const filePath = 'path/to/file.yml' + const content = Buffer.from('key: value').toString('base64') + Settings.fileCache[`${mockRepo.owner}/${filePath}`] = { etag: 'etag123', data: { content } } + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 304 }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toEqual({ key: 'value' }); + expect(result).toEqual({ key: 'value' }) expect(settings.github.repos.getContent).toHaveBeenCalledWith( expect.objectContaining({ headers: { 'If-None-Match': 'etag123' } }) - ); - }); + ) + }) it('should not return cached content when the cache is for another org', async () => { // Given - const filePath = 'path/to/file.yml'; - const content = Buffer.from('key: value').toString('base64'); - const wrongContent = Buffer.from('wrong: content').toString('base64'); - Settings.fileCache['another-org/path/to/file.yml'] = { etag: 'etag123', data: { wrongContent } }; + const filePath = 'path/to/file.yml' + const content = Buffer.from('key: value').toString('base64') + const wrongContent = Buffer.from('wrong: content').toString('base64') + Settings.fileCache['another-org/path/to/file.yml'] = { etag: 'etag123', data: { wrongContent } } jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: { content }, headers: { etag: 'etag123' } - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toEqual({ key: 'value' }); + expect(result).toEqual({ key: 'value' }) }) it('should return null when the file path is a folder', async () => { // Given - const filePath = 'path/to/folder'; + const filePath = 'path/to/folder' jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: [] - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeNull(); - }); + expect(result).toBeNull() + }) it('should return null when the file is a symlink or submodule', async () => { // Given - const filePath = 'path/to/symlink'; + const filePath = 'path/to/symlink' jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: { content: null } - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeUndefined(); - }); + expect(result).toBeUndefined() + }) it('should handle 404 errors gracefully and return null', async () => { // Given - const filePath = 'path/to/nonexistent.yml'; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 404 }); + const filePath = 'path/to/nonexistent.yml' + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 404 }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeNull(); - }); + expect(result).toBeNull() + }) it('should throw an error for non-404 exceptions when not in nop mode', async () => { // Given - const filePath = 'path/to/error.yml'; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); + const filePath = 'path/to/error.yml' + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')) // When / Then - await expect(settings.loadYaml(filePath)).rejects.toThrow('Unexpected error'); - }); + await expect(settings.loadYaml(filePath)).rejects.toThrow('Unexpected error') + }) it('should log and append NopCommand for non-404 exceptions in nop mode', async () => { // Given - const filePath = 'path/to/error.yml'; - settings.nop = true; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); - jest.spyOn(settings, 'appendToResults'); + const filePath = 'path/to/error.yml' + settings.nop = true + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')) + jest.spyOn(settings, 'appendToResults') // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeUndefined(); + expect(result).toBeUndefined() expect(settings.appendToResults).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ @@ -455,7 +452,7 @@ repository: }) }) ]) - ); - }); - }); + ) + }) + }) }) // Settings Tests diff --git a/ui/README.md b/ui/README.md index 4f3cc28d3..e8de83917 100644 --- a/ui/README.md +++ b/ui/README.md @@ -38,3 +38,32 @@ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/bui This directory contains example API routes for the headless API app. For more details, see [route.js file convention](https://nextjs.org/docs/app/api-reference/file-conventions/route). + +--- + +```mermaid + +sequenceDiagram + participant User + participant OrganizationsTable.jsx + participant HubOrgGraph.jsx + participant Next.js API Proxy + participant Backend (Express) + participant GitHub API + + User->>OrganizationsTable.jsx: Loads Organization page + OrganizationsTable.jsx->>Next.js API Proxy: GET /api/safe-settings/installation + Next.js API Proxy->>Backend (Express): GET /api/safe-settings/installation + Backend (Express)->>GitHub API: Fetch org installations, repo status, commit info, sync status + GitHub API-->>Backend (Express): Returns org data + Backend (Express)-->>Next.js API Proxy: Returns installations array + Next.js API Proxy-->>OrganizationsTable.jsx: Returns installations array + OrganizationsTable.jsx->>HubOrgGraph.jsx: Passes org data (hasConfigRepo, isInSync) + HubOrgGraph.jsx->>Next.js API Proxy: (if fetching own data) GET /api/safe-settings/installation + Next.js API Proxy->>Backend (Express): GET /api/safe-settings/installation + Backend (Express)->>GitHub API: (repeat fetch if needed) + GitHub API-->>Backend (Express): Returns org data + Backend (Express)-->>Next.js API Proxy: Returns installations array + Next.js API Proxy-->>HubOrgGraph.jsx: Returns installations array + User->>OrganizationsTable.jsx: Interacts with table/graph (tooltips, legend, etc.) +``` \ No newline at end of file diff --git a/ui/favico.ico b/ui/favico.ico deleted file mode 100644 index bf67019abb1784abbeba87886cf3e5769b53534c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ diff --git a/ui/favico.png b/ui/favico.png deleted file mode 100644 index bf67019abb1784abbeba87886cf3e5769b53534c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ diff --git a/ui/favicon.svg b/ui/favicon.svg new file mode 100644 index 000000000..17791cf9b --- /dev/null +++ b/ui/favicon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/next.config.js b/ui/next.config.js index d5d2d2c15..428ac9ab8 100644 --- a/ui/next.config.js +++ b/ui/next.config.js @@ -5,6 +5,15 @@ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, + async redirects() { + return [ + { + source: '/', + destination: '/dashboard', + permanent: false, + }, + ]; + }, }; module.exports = nextConfig; diff --git a/ui/package-lock.json b/ui/package-lock.json index 8ab1771e6..9f7381e78 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,7 +10,11 @@ "dependencies": { "@primer/octicons-react": "^19.15.5", "bootstrap": "^5.3.7", - "next": "15.4.7" + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", + "next": "^15.4.7", + "swr": "^2.3.6" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -1487,6 +1491,69 @@ "node": ">= 8" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1609,6 +1676,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -4619,6 +4695,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwindcss": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", @@ -4917,6 +5006,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index bc0097754..cfa48ab9d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack --port 3001", "build": "next build", "export": "next export", "start": "next start" @@ -11,7 +11,11 @@ "dependencies": { "@primer/octicons-react": "^19.15.5", "bootstrap": "^5.3.7", - "next": "15.4.7" + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", + "next": "^15.4.7", + "swr": "^2.3.6" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg index 7762395be..17791cf9b 100644 --- a/ui/public/favicon.svg +++ b/ui/public/favicon.svg @@ -1,3 +1,3 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/public/shield.png b/ui/public/shield.png deleted file mode 100644 index a8e4b6ec7bcbde72dcd715b6269b22e48c668976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24289 zcmXtgV|-oT^Yy*4bz?TRjmEaq*iIVTjcu!8o1|f5+qTWdcJkcs@AH4&oO9mY*?acP znl)?ojZ{{YLPo$x0002UGScFz000md{9OSD1Acb=3f=?1fG(<1qJXMN!V>`C3qVF( zMBUTitOMSYP%BmFrLC#Fbs>qN`uh7&Em1Y8nG^Px=)dxTc7K3jc#U2TxEY_p#@}EL z{kD8GH+|SiLr4jrdhv0krIA8-;7M7`$Y1|4Y(6R^oqMWy$o;!0_qiP7Z7N?#YEW-7 z(~YcnJ4t3(=xF*kUul?s?*WxWo+vC2uXnR>itj8tff5Bxt$Vy)fAMQ;bbv4`$2~?? zn6T(EcimkczP;=Dg*9%C1E;F38+Uxe=W2U*!{@OTx@1Y3miy`abuos%vD)YUV-2-G zhmH3fH%S)NY1Ykep)oL64Fp?i1QXgowe13%daXK9pV`eIIWX-l-Ld0SW-Mg#`-gcxm_?|I^CNGUY_Kli@*MoLIa_CgL+nL?n+PGp^sh$x3oxUPY%!7d4Q

    xNbM`PG=BC2pI)-gc#?ujEY|nz)%svQ;+C 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 M07*qoM6N<$g22I`vj6}9 diff --git a/ui/shield.png b/ui/shield.png index bf67019abb1784abbeba87886cf3e5769b53534c..93d8476ff0cf5d83c97b703afaffa0921f446cae 100644 GIT binary patch literal 4883 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 literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ diff --git a/ui/shield.svg b/ui/shield.svg new file mode 100644 index 000000000..17791cf9b --- /dev/null +++ b/ui/shield.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/app/components/EnvVariables.jsx b/ui/src/app/components/EnvVariables.jsx index fd7eedd15..aab69cfe1 100644 --- a/ui/src/app/components/EnvVariables.jsx +++ b/ui/src/app/components/EnvVariables.jsx @@ -19,7 +19,7 @@ export default function EnvVariables() { const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - fetch(`/api/settings/env${includeInfra ? '?includeInfra=true' : ''}`) + fetch(`/api/safe-settings/app/env${includeInfra ? '?includeInfra=true' : ''}`) .then(r => { if (!r.ok) { throw new Error(`Unable to retrieve environment variables (HTTP ${r.status}). Please try again later.`); @@ -98,23 +98,7 @@ export default function EnvVariables() { setSearch(e.target.value)} /> -

    - -
    - setIncludeInfra(e.target.checked)} /> - -
    -
    - setRevealAll(e.target.checked)} /> - -
    -
    -
    -
    - - -
    -
    + {/* Removed options and buttons section for a cleaner environment page UI */} {loading &&
    Loading…
    } diff --git a/ui/src/app/components/HubOrgGraph.jsx b/ui/src/app/components/HubOrgGraph.jsx new file mode 100644 index 000000000..bd3b42e3b --- /dev/null +++ b/ui/src/app/components/HubOrgGraph.jsx @@ -0,0 +1,140 @@ +'use client'; +import { useEffect, useRef } from "react"; +import useSWR from "swr"; + +const fetcher = (...args) => fetch(...args).then(res => res.json()); + +export default function HubOrgGraph({ width = 640, height = 320 }) { + const vizRef = useRef(null); + const { data, error } = useSWR("/api/safe-settings/installation", fetcher); + const orgs = Array.isArray(data?.installations) + ? data.installations.filter(i => i.type === "Organization") + : []; + const orgCount = orgs.length; + + useEffect(() => { + if (typeof window === "undefined" || !data) return; + Promise.all([ + import("d3-selection"), + import("d3-force"), + import("d3-drag") + ]).then(([d3Selection, d3Force, d3Drag]) => { + const select = d3Selection.select; + const forceSimulation = d3Force.forceSimulation; + const forceLink = d3Force.forceLink; + const forceManyBody = d3Force.forceManyBody; + const forceCenter = d3Force.forceCenter; + const drag = d3Drag.drag; + // Dynamic graph data: 1 HUB, N ORGs + const nodes = [ { id: "Hub", group: 1, label: "Hub", color: "#0a2540" } ]; + if (orgs.length > 0) { + orgs.forEach((org, i) => { + const orgKey = org.account; + const hasConfigRepo = org.hasConfigRepo === true; + nodes.push({ id: orgKey, group: 2, label: "ORG", color: hasConfigRepo ? "#2ea44f" : "#6a737d", tooltip: org.account }); + }); + } else { + for (let i = 1; i <= orgCount; i++) { + nodes.push({ id: `ORG${i}`, group: 2, label: "ORG", color: "#6a737d", tooltip: `ORG${i}` }); + } + } + const links = []; + if (orgs.length > 0) { + orgs.forEach((org, i) => { + const orgKey = org.account; + links.push({ source: "Hub", target: orgKey }); + }); + } else { + for (let i = 1; i <= orgCount; i++) { + links.push({ source: "Hub", target: `ORG${i}` }); + } + } + select(vizRef.current).selectAll("svg").remove(); + const svg = select(vizRef.current) + .append("svg") + .attr("width", width) + .attr("height", height); + const simulation = forceSimulation(nodes) + .force("link", forceLink(links).id(d => d.id).distance(120)) + .force("charge", forceManyBody().strength(-400)) + .force("center", forceCenter(width / 2, height / 2)); + const link = svg.append("g") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6) + .selectAll("line") + .data(links) + .join("line") + .attr("stroke-width", 2); + const node = svg.append("g") + .attr("stroke", "#fff") + .attr("stroke-width", 2) + .selectAll("circle") + .data(nodes) + .join("circle") + .attr("r", 24) + .attr("fill", d => d.group === 1 ? d.color : d.color || "#6f42c1") + .call(drag() + .on("start", (event, d) => { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; d.fy = d.y; + }) + .on("drag", (event, d) => { + d.fx = event.x; d.fy = event.y; + }) + .on("end", (event, d) => { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; d.fy = null; + }) + ); + node.append("title") + .text(d => d.group === 2 ? d.tooltip : "Hub"); + const label = svg.append("g") + .selectAll("text") + .data(nodes) + .join("text") + .attr("text-anchor", "middle") + .attr("dy", ".35em") + .attr("font-size", 16) + .attr("font-family", "sans-serif") + .attr("fill", d => d.group === 1 ? "#fff" : "#fff") + .text(d => d.label) + .each(function(d) { + d3Selection.select(this) + .append("title") + .text(d.group === 2 ? d.tooltip : "Hub"); + }); + simulation.on("tick", () => { + link + .attr("x1", d => d.source.x) + .attr("y1", d => d.source.y) + .attr("x2", d => d.target.x) + .attr("y2", d => d.target.y); + node + .attr("cx", d => d.x) + .attr("cy", d => d.y); + label + .attr("x", d => d.x) + .attr("y", d => d.y); + }); + }); + }, [width, height, orgCount, data]); + + if (error) return
    Error loading organization graph.
    ; + if (!data) return
    Loading organization graph...
    ; + + return ( +
    +
    +
    + + + Has safe-settings admin repo + + + + No safe-settings admin repo + +
    +
    + ); +} diff --git a/ui/src/app/components/OrganizationsTable.jsx b/ui/src/app/components/OrganizationsTable.jsx index 56bd12c41..f6e394ae5 100644 --- a/ui/src/app/components/OrganizationsTable.jsx +++ b/ui/src/app/components/OrganizationsTable.jsx @@ -1,73 +1,117 @@ -'use client'; +"use client"; -import React, { useState, useMemo, useEffect } from 'react'; -import { ChevronUpIcon, ChevronDownIcon, SearchIcon } from '@primer/octicons-react'; -import { useHydrated } from '../hooks/useHydrated'; +import React, { useState, useMemo, useEffect, useRef } from "react"; +import { + ChevronUpIcon, + ChevronDownIcon, + SearchIcon, + InfoIcon, +} from "@primer/octicons-react"; +import { useHydrated } from "../hooks/useHydrated"; -// Mock organizations used when /api/organizations returns 404 +// Mock organizations used when /api/safe-settings/installation returns 404 const MOCK_ORGS = [ - { id: 1, name: 'mock-org-one', lastSyncDate: new Date(Date.now() - 3600 * 1000).toISOString(), lastSyncMessage: 'Initial mock sync', lastSyncSha: 'abcdef1', ageSeconds: 3600 }, - { id: 2, name: 'example-inc', lastSyncDate: new Date(Date.now() - 7200 * 1000).toISOString(), lastSyncMessage: 'Second mock sync', lastSyncSha: 'abcdef2', ageSeconds: 7200 }, - { id: 3, name: 'demo-labs', lastSyncDate: null, lastSyncMessage: null, lastSyncSha: null, ageSeconds: null, na: true } + { + id: 1, + name: "mock-org-one", + lastSyncDate: new Date(Date.now() - 3600 * 1000).toISOString(), + lastSyncMessage: "Initial mock sync", + lastSyncSha: "abcdef1", + ageSeconds: 3600, + }, + { + id: 2, + name: "example-inc", + lastSyncDate: new Date(Date.now() - 7200 * 1000).toISOString(), + lastSyncMessage: "Second mock sync", + lastSyncSha: "abcdef2", + ageSeconds: 7200, + }, + { + id: 3, + name: "demo-labs", + lastSyncDate: null, + lastSyncMessage: null, + lastSyncSha: null, + ageSeconds: null, + na: true, + }, ]; const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, setSearchTerm] = useState(""); const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [fetched, setFetched] = useState([]); const hydrated = useHydrated(); + const [selectedIds, setSelectedIds] = useState(() => new Set()); + const headerCheckboxRef = useRef(null); + const [retrievingFiles, setRetrievingFiles] = useState(false); + const [retrieveMessage, setRetrieveMessage] = useState(null); + const [retrieveError, setRetrieveError] = useState(null); + const [retrieveResults, setRetrieveResults] = useState(null); // Fetch real organizations from backend API on client hydration useEffect(() => { if (!hydrated) return; // avoid SSR mismatch let cancelled = false; setLoading(true); - fetch('/api/organizations') - .then(r => { + + fetch("/api/safe-settings/installation") + .then((r) => { if (!r.ok) { - throw new Error(`Unable to retrieve organizations (HTTP ${r.status}). Please try again later.`); + throw new Error( + `Unable to retrieve organizations (HTTP ${r.status}). Please try again later.` + ); } return r.json(); }) - .then(json => { + .then((json) => { if (!json || cancelled) return; - const lastCommits = json.lastCommits || {} - const mapped = (json.installations || []).map(i => { - const lc = lastCommits[i.account]; - return { - id: i.id, - name: i.account, - lastSyncDate: lc && lc.committed_at ? lc.committed_at : null, - lastSyncSha: lc && lc.sha ? lc.sha : null, - lastSyncMessage: lc && lc.message ? lc.message : null, - ageSeconds: lc && typeof lc.age_seconds === 'number' ? lc.age_seconds : null, - na: lc && lc.na === true - }; - }); + const mapped = (json.installations || []).map((i) => ({ + id: i.id, + name: i.account, + lastSyncDate: i.committed_at || null, + lastSyncSha: i.sha || null, + lastSyncMessage: i.message || null, + ageSeconds: typeof i.age_seconds === "number" ? i.age_seconds : null, + hasConfigRepo: + typeof i.hasConfigRepo === "boolean" ? i.hasConfigRepo : false, + isInSync: typeof i.isInSync === "boolean" ? i.isInSync : false, + })); setFetched(mapped); setError(null); }) - .catch(e => { if (!cancelled) setError(e.message); }) - .finally(() => { if (!cancelled) setLoading(false); }); - return () => { cancelled = true; }; + .catch((e) => { + if (!cancelled) setError(e.message); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; }, [hydrated]); - const data = fetched.length > 0 ? fetched : (propOrganizations.length > 0 ? propOrganizations : []); + const data = + fetched.length > 0 + ? fetched + : propOrganizations.length > 0 + ? propOrganizations + : []; // Format date for display with hydration-safe approach const formatLastSync = (org) => { - if (org.na) return NA; if (!org.lastSyncDate) return ; const dateObj = new Date(org.lastSyncDate); let ageSec = org.ageSeconds; - if (hydrated && (ageSec == null)) { + if (hydrated && ageSec == null) { ageSec = Math.floor((Date.now() - dateObj.getTime()) / 1000); } const rel = (() => { - if (ageSec == null) return ''; - if (ageSec < 60) return '0m'; + if (ageSec == null) return ""; + if (ageSec < 60) return "0m"; const mTotal = Math.floor(ageSec / 60); if (mTotal < 60) return `${mTotal}m`; const hTotal = Math.floor(mTotal / 60); @@ -79,17 +123,35 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { const remH = hTotal % 24; return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; })(); - const fullStamp = `${dateObj.getFullYear()}-${String(dateObj.getMonth()+1).padStart(2,'0')}-${String(dateObj.getDate()).padStart(2,'0')} ${String(dateObj.getHours()).padStart(2,'0')}:${String(dateObj.getMinutes()).padStart(2,'0')}:${String(dateObj.getSeconds()).padStart(2,'0')}`; - const tooltip = [fullStamp, org.lastSyncMessage, org.lastSyncSha ? `SHA: ${org.lastSyncSha.slice(0,7)}` : null] + const fullStamp = `${dateObj.getFullYear()}-${String( + dateObj.getMonth() + 1 + ).padStart(2, "0")}-${String(dateObj.getDate()).padStart(2, "0")} ${String( + dateObj.getHours() + ).padStart(2, "0")}:${String(dateObj.getMinutes()).padStart( + 2, + "0" + )}:${String(dateObj.getSeconds()).padStart(2, "0")}`; + const tooltip = [ + fullStamp, + org.lastSyncMessage, + org.lastSyncSha ? `SHA: ${org.lastSyncSha.slice(0, 7)}` : null, + ] .filter(Boolean) - .join('\n'); - return {rel}; + .join("\n"); + return ( + + {rel} + + ); + }; + const lastSyncColStyle = { + width: "170px", + fontVariantNumeric: "tabular-nums", }; - const lastSyncColStyle = { width: '170px', fontVariantNumeric: 'tabular-nums' }; // Filter organizations based on search term const filteredData = useMemo(() => { - return data.filter(org => + return data.filter((org) => org.name.toLowerCase().includes(searchTerm.toLowerCase()) ); }, [data, searchTerm]); @@ -103,16 +165,16 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { let bValue = b[sortConfig.key]; // Convert dates to timestamps for comparison - if (sortConfig.key === 'lastSyncDate') { + if (sortConfig.key === "lastSyncDate") { aValue = new Date(aValue).getTime(); bValue = new Date(bValue).getTime(); } if (aValue < bValue) { - return sortConfig.direction === 'asc' ? -1 : 1; + return sortConfig.direction === "asc" ? -1 : 1; } if (aValue > bValue) { - return sortConfig.direction === 'asc' ? 1 : -1; + return sortConfig.direction === "asc" ? 1 : -1; } return 0; }); @@ -120,30 +182,99 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { // Handle column sorting const handleSort = (key) => { - setSortConfig(prevConfig => { + setSortConfig((prevConfig) => { if (prevConfig.key === key) { - if (prevConfig.direction === 'asc') { - return { key, direction: 'desc' }; - } else if (prevConfig.direction === 'desc') { + if (prevConfig.direction === "asc") { + return { key, direction: "desc" }; + } else if (prevConfig.direction === "desc") { return { key: null, direction: null }; } } - return { key, direction: 'asc' }; + return { key, direction: "asc" }; }); }; // Render sort icon const renderSortIcon = (columnKey) => { if (sortConfig.key !== columnKey) { - return ; + return ( + + ↕ + + ); } - if (sortConfig.direction === 'asc') { + if (sortConfig.direction === "asc") { return ; } - if (sortConfig.direction === 'desc') { + if (sortConfig.direction === "desc") { return ; } - return ; + return ( + + ↕ + + ); + }; + + // Keep header checkbox indeterminate when some but not all rows are selected + useEffect(() => { + if (!headerCheckboxRef || !headerCheckboxRef.current) return; + const selectableCount = sortedData.filter((o) => !o.synced).length; + headerCheckboxRef.current.indeterminate = + selectedIds.size > 0 && selectedIds.size < selectableCount; + }, [selectedIds, sortedData]); + + // Prune selection when the displayed dataset changes (remove ids that no longer exist) + useEffect(() => { + setSelectedIds((prev) => { + const allowed = new Set( + sortedData.filter((o) => !o.synced).map((o) => o.id) + ); + const next = new Set([...prev].filter((id) => allowed.has(id))); + if (next.size === prev.size) return prev; + return next; + }); + }, [sortedData]); + + // Retrieve files for selected organizations + const retrieveFilesForSelected = async () => { + if (selectedIds.size === 0) return; + // map selected ids back to organization names using the current sorted/filtered dataset + const orgNames = sortedData + .filter((o) => selectedIds.has(o.id)) + .map((o) => o.name); + if (orgNames.length === 0) return; + setRetrieveResults(null); + setRetrieveMessage(null); + setRetrieveError(null); + setRetrievingFiles(true); + try { + const res = await fetch("/api/safe-settings/hub/import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ orgs: orgNames }), + }); + if (!res.ok) throw new Error(`Request failed (HTTP ${res.status})`); + const json = await res.json().catch(() => ({})); + if (Array.isArray(json.results)) { + setRetrieveResults(json.results); + const created = json.results.filter((r) => r.pr).length; + const skipped = json.results.filter((r) => r.skipped).map((r) => r.org); + const errors = json.results.filter((r) => r.error).length; + const parts = []; + if (created) + parts.push(`${created} PR${created > 1 ? "s" : ""} created`); + if (skipped.length) parts.push(`Skipped: ${skipped.join(", ")}`); + if (errors) parts.push(`${errors} error${errors > 1 ? "s" : ""}`); + setRetrieveMessage(parts.join(" • ") || "Retrieval completed"); + } else { + setRetrieveMessage(json.message || "Retrieval requested"); + } + } catch (e) { + setRetrieveError(e.message || String(e)); + } finally { + setRetrievingFiles(false); + } }; return ( @@ -166,67 +297,263 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => {
    - - Showing {sortedData.length} of {data.length} organizations - +
    + + + + +
    + {/* Reserved message area: keeps layout stable when messages appear */} +
    + {retrieveResults ? ( + retrieveResults.map((r) => ( +
    + {r.pr ? ( +
    + Imported {r.org}:{" "} + + {r.pr} + +
    + ) : r.skipped ? ( +
    + Skipping {r.org}: already present in hub +
    + ) : r.error ? ( +
    + {r.org}: {r.error} +
    + ) : null} +
    + )) + ) : ( + <> + {retrieveMessage && ( +
    {retrieveMessage}
    + )} + {retrieveError && ( +
    {retrieveError}
    + )} + + )} +
    + {/* Table */}
    - + + + {loading && ( - + )} {!loading && error && ( - + )} - {!loading && !error && sortedData.length > 0 ? ( - sortedData.map((org) => ( - - - - - )) - ) : ( - !loading && !error && ( - - - - ) - )} + {!loading && !error && sortedData.length > 0 + ? sortedData.map((org) => { + return ( + + + + + + + + ); + }) + : !loading && + !error && ( + + + + )}
    handleSort('name')} + + {/* compute selectable rows so header/select-all ignores already-imported orgs */} + { + const selectableCount = sortedData.filter( + (o) => !o.synced + ).length; + return ( + selectableCount > 0 && + selectedIds.size === selectableCount + ); + }, [sortedData, selectedIds])} + onChange={() => { + // toggle all selectable (non-synced) rows + setSelectedIds((prev) => { + const selectable = sortedData + .filter((o) => !o.synced) + .map((o) => o.id); + if (prev.size === selectable.length) return new Set(); + return new Set(selectable); + }); + }} + aria-label="Select all organizations" + /> + handleSort("name")} + > +
    +
    Organization Name
    +
    {renderSortIcon("name")}
    + + Showing {sortedData.length} of {data.length} organizations + +
    +
    - Organization Name - {renderSortIcon('name')} + Config Repo + + In Sync handleSort('lastSyncDate')} + style={{ cursor: "pointer", ...lastSyncColStyle }} + onClick={() => handleSort("lastSyncDate")} > - Last Safe-settings Sync - {renderSortIcon('lastSyncDate')} + Last Sync + {renderSortIcon("lastSyncDate")}
    Loading organizations… + Loading organizations… +
    Error: {error} +
    + {error} +
    +
    - {org.name} - - {formatLastSync(org)} -
    - {searchTerm ? `No organizations found matching "${searchTerm}"` : 'No organizations available'} -
    + + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(org.id)) next.delete(org.id); + else next.add(org.id); + return next; + }) + } + aria-label={`Select ${org.name}`} + disabled={org.synced === true} + style={ + org.synced + ? { opacity: 0.45, cursor: "not-allowed" } + : {} + } + /> + + {org.name} + {org.synced && ( + Imported + )} + + {org.hasConfigRepo ? ( + + ✓ + + ) : ( + + NA + + )} + + {org.isInSync ? ( + + ✓ + + ) : ( + + ✗ + + )} + + {formatLastSync(org)} +
    + {searchTerm + ? `No organizations found matching "${searchTerm}"` + : "No organizations available"} +
    @@ -238,8 +565,11 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { {searchTerm && `Filtered by: "${searchTerm}"`} {sortConfig.key && ( - • Sorted by: {sortConfig.key === 'name' ? 'Organization Name' : 'Last Safe-settings Sync'} - ({sortConfig.direction === 'asc' ? 'A-Z' : 'Z-A'}) + • Sorted by:{" "} + {sortConfig.key === "name" + ? "Organization Name" + : "Last Safe-settings Sync"} + ({sortConfig.direction === "asc" ? "A-Z" : "Z-A"}) )} diff --git a/ui/src/app/components/Safe-settings-hubContent.jsx b/ui/src/app/components/Safe-settings-hubContent.jsx index 3a40154c2..ada553614 100644 --- a/ui/src/app/components/Safe-settings-hubContent.jsx +++ b/ui/src/app/components/Safe-settings-hubContent.jsx @@ -6,18 +6,6 @@ import { useHydrated } from '../hooks/useHydrated'; // Match the left index width and reuse for the search input const LEFT_COL_WIDTH = 320; -const MOCK_TREE = { - name: '.github', - path: '.github', - type: 'dir', - lastCommitAt: new Date().toISOString(), - entries: [ - { name: 'CODEOWNERS', path: '.github/CODEOWNERS', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'add CODEOWNERS' }, - { name: 'workflows', path: '.github/workflows', type: 'dir', lastCommitAt: new Date().toISOString(), entries: [ - { name: 'ci.yml', path: '.github/workflows/ci.yml', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'ci: add' } - ] } - ] -}; export default function SafeSettingsHubContent3b() { const hydrated = useHydrated(); @@ -32,13 +20,13 @@ export default function SafeSettingsHubContent3b() { const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - fetch('/api/safe-settings-hub/content?fetchContent=true') + fetch('/api/safe-settings/hub/content?fetchContent=true') .then(r => { if (!r.ok) throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status})`); return r.json(); }) .then(json => { setRootTree(json); setLastFetchedAt(new Date()); }) - .catch(() => setRootTree(MOCK_TREE)) + .catch((error) => { setError("Unable to load content. Please try again later."); setRootTree(null); }) .finally(() => setLoading(false)); }; @@ -48,10 +36,16 @@ export default function SafeSettingsHubContent3b() { if (!node) return null; const term = search.toLowerCase(); const matches = (n) => !term || (n.name && n.name.toLowerCase().includes(term)) || (n.path && n.path.toLowerCase().includes(term)); - if (node.type === 'file') return matches(node) ? node : null; + if (node.type === 'file') { + return matches(node) ? node : null; + } if (node.type === 'dir') { - const children = (node.entries || []).map(filterTree).filter(Boolean); - if (matches(node) || children.length) return { ...node, entries: children }; + const filteredEntries = (node.entries || []) + .map(child => filterTree(child)) + .filter(Boolean); + if (matches(node) || filteredEntries.length > 0) { + return { ...node, entries: filteredEntries }; + } return null; } return null; diff --git a/ui/src/app/components/TitleBar.css b/ui/src/app/components/TitleBar.css index d10a7ea76..df856eff7 100644 --- a/ui/src/app/components/TitleBar.css +++ b/ui/src/app/components/TitleBar.css @@ -4,7 +4,7 @@ .title-header { background: #333; color: #fff; - min-height: 60px; /* Ensure consistent height */ + min-height: 40px; /* Ensure consistent height */ } /* Theme-specific header styles */ @@ -22,7 +22,7 @@ body.dark-theme .title-header { /* Navigation bar - consistent height and styling */ .title-nav { - min-height: 48px; /* Consistent nav height */ + min-height: 40px; /* Consistent nav height */ border-bottom: 1px solid var(--border-color, #dee2e6) !important; background: var(--bg-secondary, #f6f8fa); /* Default light theme background */ } @@ -102,7 +102,7 @@ body.dark-theme .nav-link-custom { /* Navigation menu items */ .nav-link.menu-hover { border-radius: 5px !important; - margin: 10px 10px 10px 10px !important; + margin: 10px 10px 9px 10px !important; padding: 5px 10px !important; transition: background 0.15s, color 0.15s; border: 1px solid transparent !important; /* Invisible border to maintain box model */ diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx index 818f4a356..f24b10862 100644 --- a/ui/src/app/components/TitleBar.jsx +++ b/ui/src/app/components/TitleBar.jsx @@ -1,9 +1,16 @@ "use client"; import { usePathname } from "next/navigation"; import React from "react"; -import { GlobeIcon, GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; -import { useTheme } from './ThemeContext'; -import './TitleBar.css'; +import { + GlobeIcon, + GearIcon, + ListUnorderedIcon, + SunIcon, + MoonIcon, + NoteIcon, +} from "@primer/octicons-react"; +import { useTheme } from "./ThemeContext"; +import "./TitleBar.css"; export default function TitleBar() { const pathname = usePathname(); @@ -12,31 +19,52 @@ export default function TitleBar() { // Always render the TitleBar structure to prevent layout shift return ( <> -
    +
    - - + + - Safe-Settings Dashboard + + Safe-Settings Hub Dashboard + - +
    + +
    diff --git a/ui/src/app/dashboard/help/page.jsx b/ui/src/app/dashboard/help/page.jsx new file mode 100644 index 000000000..866aa7fae --- /dev/null +++ b/ui/src/app/dashboard/help/page.jsx @@ -0,0 +1,35 @@ +'use client'; + +import TitleBar from "../../components/TitleBar"; +import Link from "next/link"; +import HubOrgGraph from "../../components/HubOrgGraph"; + +export default function HelpPage() { + return ( +
    + +
    +

    Dashboard & Hub - Help

    +

    Quick guidance for the Safe-Settings Dashboard and Hub.

    + +

    +

    What is the Safe-Settings Dashboard

    +

    + This UI provides status information for the Safe-Settings Hub feature. It is a read-first reporting and status tool that displays configuration state and import/sync status. +

    +

    How to get started

    +

    + The Organizations page lists every Org where the Safe-Settings Hub is installed. You can use the Retrieve Settings button to perform an initial import from the selected organizations' config repositories. It reads files from the configured CONFIG_PATH in each organization's config repo and commits them into a single branch in the hub repository, then opens a pull request for review. This is intended for initial population or one-time imports — the action will skip organizations that already have content in the hub path. +

    +

    How to edit configuration

    +

    + The dashboard is not a content editor. To change configuration you should edit files in your admin repository and follow the normal GitHub workflow: commit changes, open a pull request, get required approvers to review, and merge. After the PR is merged the dashboard will reflect the updated state. +

    +
    +

    + If you need more help, check the repository documentation or contact the maintainers. +

    +
    +
    + ); +} diff --git a/ui/src/app/dashboard/organizations/page.jsx b/ui/src/app/dashboard/organizations/page.jsx index e5712bf0e..596f9299f 100644 --- a/ui/src/app/dashboard/organizations/page.jsx +++ b/ui/src/app/dashboard/organizations/page.jsx @@ -11,7 +11,7 @@ export default function OrganizationsPage() { Organizations

    - List all the installations of the App and the last time Safe-settings configurations were synced. + List all the Organizations where the Safe-Settings App is installed and the last time Safe-settings configurations were synced.

    diff --git a/ui/src/app/dashboard/page.jsx b/ui/src/app/dashboard/page.jsx index 92cd1ac12..7e4787fc5 100644 --- a/ui/src/app/dashboard/page.jsx +++ b/ui/src/app/dashboard/page.jsx @@ -1,4 +1,5 @@ import TitleBar from "../components/TitleBar"; +import { AlertIcon, ArrowRightIcon, CheckCircleIcon, GitCommitIcon, GitPullRequestIcon, GitMergeIcon, EyeIcon } from "@primer/octicons-react"; export default function DashboardPage() { return ( @@ -7,6 +8,17 @@ export default function DashboardPage() {

    Welcome to the Safe-Settings Hub Dashboard

    Select a menu item above to get started.

    +

    + This dashboard is a read-first reporting interface that displays configuration state and sync activity status for the Safe-Settings Hub.
    +
    It is not intended as the workflow for editing Safe-Settings Hub configuration content.

    + +
    To make changes, please use the standard GitHub process for content updates:


    + Commit        + Pull Request        + Approve        + Merge         + +

    ); diff --git a/ui/src/app/dashboard/safe-settings-hub/page.jsx b/ui/src/app/dashboard/safe-settings-hub/page.jsx index a8bbe5810..56a4ef355 100644 --- a/ui/src/app/dashboard/safe-settings-hub/page.jsx +++ b/ui/src/app/dashboard/safe-settings-hub/page.jsx @@ -12,7 +12,7 @@ export default function SafeSettingsHubConfigPage() {

    Listing files maintained by the Safe-Settings Global configuration (all ORG's). - Files are retrieved from `/api/safe-settings-hub/content`. + Files are retrieved from `/api/safe-settings/hub/content`.


    diff --git a/ui/src/app/dashboard/settings/page.jsx b/ui/src/app/dashboard/settings/page.jsx deleted file mode 100644 index e63d76620..000000000 --- a/ui/src/app/dashboard/settings/page.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import TitleBar from "../../components/TitleBar"; - -export default function SettingsPage() { - return ( -
    - -
    -

    Settings

    -

    Settings options will go here.

    -
    -
    - ); -} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index 46564ec39..09a592e7c 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -43,7 +43,7 @@ body.dark-theme { --bg-secondary: #444444; --bg-accent: #30363d; --text-primary: #f0f6fc; - --text-secondary: #e3e3e3; + --text-secondary: #b3b3b3; } /* Global Theme Styles */ @@ -56,32 +56,32 @@ body { /* Theme-specific body styles using data-theme */ [data-theme="light"] body, body.light-theme { - background: #fff; - color: #24292f; + background: #fff !important; + color: var(--text-primary) !important; } [data-theme="dark"] body, body.dark-theme { - background: rgb(45, 46, 47); - color: #f6f8fa; + background: rgb(24, 24, 24) !important; + color: var(--text-primary) !important; } /* Global Main Element Theme */ [data-theme="light"] main, body.light-theme main { - background: #fff; - color: #24292f; + background: #fff !important; + color: var(--text-primary) !important; } [data-theme="dark"] main, body.dark-theme main { /* background: #161b22; */ - color: #f6f8fa; + color: var(--text-primary) !important; } [data-theme="light"] .nav-link, body.light-theme .nav-link { - color: #24292f; + color: var(--text-primary) !important; } [data-theme="dark"] .nav-link, @@ -106,7 +106,7 @@ body.light-theme .nav-tabs { main { color: var(--text-primary) !important; /* padding: 1rem; */ - border-radius: 12px; + border-radius: 12px !important; /* margin-top: 1rem; */ } @@ -147,11 +147,11 @@ main { /* Global Font Utility Classes */ .dark-font { - color: #f6f8fa; + color: var(--text-primary) !important; } .light-font { - color: #24292f; + color: var(--text-primary) !important; } /* Organizations Table Styles */ diff --git a/ui/src/app/route.js b/ui/src/app/route.js deleted file mode 100644 index af7296c8e..000000000 --- a/ui/src/app/route.js +++ /dev/null @@ -1,7 +0,0 @@ -const { NextResponse } = require('next/server'); - -export async function GET() { - return NextResponse.json({ message: 'Hello world!' }); -} - -export const dynamic = 'force-static'; From dad3fe837ead99e862946d9f3aad899e0542a074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Wed, 24 Sep 2025 15:19:09 -0400 Subject: [PATCH 4/5] Add ui screen --- hubSyncHandler.log | 754 +++++++++++++++++++++++++++++ lib/hubSyncHandler.js | 492 ++++++++++--------- lib/routes.js | 48 ++ package-lock.json | 41 +- package.json | 2 +- safe-settings.log | 54 +++ ui/public/favicon.ico | 0 ui/src/app/api/logs/route.js | 28 ++ ui/src/app/components/TitleBar.jsx | 46 +- ui/src/app/dashboard/logs/page.jsx | 110 +++++ ui/src/app/globals.css | 8 + 11 files changed, 1306 insertions(+), 277 deletions(-) create mode 100644 hubSyncHandler.log create mode 100644 safe-settings.log create mode 100644 ui/public/favicon.ico create mode 100644 ui/src/app/api/logs/route.js create mode 100644 ui/src/app/dashboard/logs/page.jsx diff --git a/hubSyncHandler.log b/hubSyncHandler.log new file mode 100644 index 000000000..84e9592ce --- /dev/null +++ b/hubSyncHandler.log @@ -0,0 +1,754 @@ +2025-09-11T15:47:52.340Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-11T15:47:52.339Z [INFO] Received 'pull_request.closed' event: 47 +2025-09-11T15:47:52.636Z [INFO] Files changed in PR #47: .github/safe-settings/globals/suborg.yml +2025-09-11T15:47:52.637Z [INFO] Syncing safe settings for 'globals/'. +2025-09-11T15:47:52.636Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-11T15:47:52.864Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-11T15:47:53.106Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-11T15:47:53.106Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-11T15:47:53.106Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml +2025-09-11T15:47:53.434Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:47:53.680Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:47:53.681Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-11T15:47:53.681Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) +2025-09-11T15:47:54.039Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:47:54.273Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:47:54.273Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) +2025-09-11T15:47:54.273Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-11T15:47:54.585Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:47:54.886Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:47:54.886Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) +2025-09-11T15:47:54.886Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-11T15:47:55.093Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-11T15:47:55.093Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-11T15:47:55.511Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:47:55.758Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:47:55.759Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-11T15:47:55.759Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) +2025-09-11T15:47:55.933Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-11T15:47:55.933Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-11T15:47:56.108Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. +2025-09-11T15:47:59.386Z [DEBUG] Pull_request REopened ! +2025-09-11T15:47:59.386Z [DEBUG] Is Admin repo event false +2025-09-11T15:47:59.386Z [DEBUG] Not working on the Admin repo, returning... +2025-09-11T15:49:09.315Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-11T15:49:09.315Z [DEBUG] Branch Protection edited by a Human +2025-09-11T15:49:09.315Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-11T15:49:09.553Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} +2025-09-11T15:49:24.081Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-11T15:49:24.081Z [INFO] Received 'pull_request.closed' event: 47 +2025-09-11T15:49:24.356Z [INFO] Files changed in PR #47: .github/safe-settings/globals/suborg.yml +2025-09-11T15:49:24.356Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-11T15:49:24.357Z [INFO] Syncing safe settings for 'globals/'. +2025-09-11T15:49:24.617Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-11T15:49:24.814Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-11T15:49:24.814Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml +2025-09-11T15:49:24.814Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-11T15:49:25.155Z [DEBUG] Is Admin repo event false +2025-09-11T15:49:25.155Z [DEBUG] Not working on the Admin repo, returning... +2025-09-11T15:49:25.341Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:49:25.565Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:49:25.566Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-11T15:49:25.566Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) +2025-09-11T15:49:25.935Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:49:26.172Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:49:26.173Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-11T15:49:26.173Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) +2025-09-11T15:49:26.524Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:49:26.777Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:49:26.777Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-11T15:49:26.777Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) +2025-09-11T15:49:26.964Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-11T15:49:26.964Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-11T15:49:27.285Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:49:27.487Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:49:27.487Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-11T15:49:27.487Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) +2025-09-11T15:49:27.661Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-11T15:49:27.661Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-11T15:49:27.830Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. +2025-09-11T15:50:54.611Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:50:54.611Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:55.683Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:55.683Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:50:56.556Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:50:56.556Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:57.768Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:57.768Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:51:20.652Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:51:20.652Z [DEBUG] Branch Protection edited by Bot +2025-09-11T15:51:24.560Z [DEBUG] Not triggered by Safe-settings... +2025-09-11T15:51:24.559Z [DEBUG] Check run was created! +2025-09-11T15:51:35.514Z [DEBUG] Branch Protection edited by Bot +2025-09-11T15:51:35.514Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:51:36.562Z [DEBUG] Not triggered by Safe-settings... +2025-09-11T15:51:36.562Z [DEBUG] Check run was created! +2025-09-11T15:53:20.953Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-11T15:53:22.397Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-11T15:53:22.397Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-11T15:53:22.397Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-11T15:53:22.628Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:53:22.628Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:22.628Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-11T15:53:22.859Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-11T15:53:22.859Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:53:23.452Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-11T15:53:23.452Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-11T15:53:23.452Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-11T15:53:23.691Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:53:23.691Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-11T15:53:23.691Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:23.944Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-11T15:53:23.944Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:53:24.491Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-11T15:53:24.491Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-11T15:53:24.491Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-11T15:53:24.733Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:24.733Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:53:24.733Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-11T15:53:25.050Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-11T15:53:25.050Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:53:25.516Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:53:25.516Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-11T15:53:25.516Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-11T15:53:25.516Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:53:25.985Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:53:25.985Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-11T15:53:25.985Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:53:26.589Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-11T15:53:26.589Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-11T15:53:26.589Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-11T15:53:26.825Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-11T15:53:26.825Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:53:26.825Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:27.058Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-11T15:53:27.058Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:55:44.833Z [INFO] Retrieving settings from org: jetest99 +2025-09-11T15:55:45.087Z [INFO] Skipping jetest99: already present in hub +2025-09-11T15:55:45.087Z [INFO] Retrieving settings from org: jefeish-training +2025-09-11T15:55:45.298Z [INFO] Retrieving settings from org: jefeish-test1 +2025-09-11T15:55:45.298Z [INFO] Skipping jefeish-training: already present in hub +2025-09-11T15:55:45.551Z [INFO] Retrieving settings from org: copilot-for-emus +2025-09-11T15:55:45.551Z [INFO] Skipping jefeish-test1: already present in hub +2025-09-11T15:55:46.001Z [INFO] Retrieving settings from org: jefeish-migration-test +2025-09-11T15:55:46.292Z [INFO] Retrieving settings from org: decyjphr-training +2025-09-11T15:55:46.292Z [INFO] Skipping jefeish-migration-test: already present in hub +2025-09-11T15:55:46.556Z [INFO] Skipping decyjphr-training: already present in hub +2025-09-11T15:55:46.556Z [INFO] Retrieving settings from org: decyjphr-emu +2025-09-11T15:56:33.309Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-11T15:56:34.976Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-11T15:56:34.976Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-11T15:56:34.976Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-11T15:56:35.221Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-11T15:56:35.221Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:35.221Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:35.434Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-11T15:56:35.434Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:36.047Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-11T15:56:36.047Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-11T15:56:36.047Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-11T15:56:36.273Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:36.273Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:36.273Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-11T15:56:36.514Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:36.514Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-11T15:56:37.018Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-11T15:56:37.018Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-11T15:56:37.018Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-11T15:56:37.239Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:37.239Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-11T15:56:37.239Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:37.445Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-11T15:56:37.445Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:37.914Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-11T15:56:37.914Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:37.914Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-11T15:56:37.914Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:38.412Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:38.412Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:38.412Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-11T15:56:38.977Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-11T15:56:38.977Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-11T15:56:38.977Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-11T15:56:39.247Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:39.247Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:39.247Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-11T15:56:39.484Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:39.485Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-11T15:56:51.776Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-11T15:56:53.217Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-11T15:56:53.217Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-11T15:56:53.217Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-11T15:56:53.436Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:53.436Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:53.436Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-11T15:56:53.666Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-11T15:56:53.666Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:54.354Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-11T15:56:54.354Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-11T15:56:54.354Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-11T15:56:54.566Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:54.566Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-11T15:56:54.566Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:54.792Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:54.792Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-11T15:56:55.340Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-11T15:56:55.340Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-11T15:56:55.340Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-11T15:56:55.563Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:55.563Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:55.563Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-11T15:56:55.807Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:55.808Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-11T15:56:56.233Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-11T15:56:56.233Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:56.233Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:56.233Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-11T15:56:56.688Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-11T15:56:56.688Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:56.688Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:57.315Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-11T15:56:57.315Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-11T15:56:57.314Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-11T15:56:57.525Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:57.525Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:57.525Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-11T15:56:57.745Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:57.745Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-12T01:30:19.210Z [DEBUG] Check run was created! +2025-09-12T01:30:19.210Z [DEBUG] Not triggered by Safe-settings... +2025-09-13T22:42:46.364Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-13T22:42:48.651Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-13T22:42:48.651Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-13T22:42:48.651Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-13T22:42:48.902Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:48.902Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:42:48.902Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-13T22:42:49.124Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:42:49.124Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-13T22:42:49.899Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-13T22:42:49.899Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-13T22:42:49.899Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-13T22:42:50.157Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:50.157Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:42:50.157Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-13T22:42:50.373Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-13T22:42:50.373Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:42:51.186Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-13T22:42:51.186Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-13T22:42:51.186Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-13T22:42:51.533Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:42:51.533Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-13T22:42:51.533Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:51.901Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:42:51.901Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-13T22:42:52.487Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:42:52.487Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:42:52.487Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-13T22:42:52.487Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-13T22:42:52.978Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:42:52.979Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:42:52.978Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-13T22:42:53.877Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-13T22:42:53.877Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-13T22:42:53.877Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-13T22:42:54.131Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:54.131Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:42:54.131Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-13T22:42:54.397Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:42:54.397Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-13T22:43:30.372Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-13T22:43:34.138Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-13T22:43:34.138Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-13T22:43:34.138Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-13T22:43:34.350Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:34.350Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-13T22:43:34.350Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:43:34.574Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:43:34.574Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-13T22:43:35.156Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-13T22:43:35.156Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-13T22:43:35.156Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-13T22:43:35.390Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:43:35.390Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:35.390Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-13T22:43:35.778Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:43:35.778Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-13T22:43:36.334Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-13T22:43:36.334Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-13T22:43:36.334Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-13T22:43:36.548Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:36.548Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:43:36.548Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-13T22:43:36.780Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:43:36.780Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-13T22:43:37.236Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:43:37.236Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:43:37.236Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-13T22:43:37.236Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-13T22:43:37.666Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:43:37.666Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-13T22:43:37.666Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:43:38.247Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-13T22:43:38.247Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-13T22:43:38.247Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-13T22:43:38.457Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:43:38.457Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-13T22:43:38.457Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:38.763Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:43:38.763Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-16T15:47:20.575Z [INFO] Received 'pull_request.closed' event: 10 +2025-09-16T15:47:20.577Z [INFO] Pull request.closed is not from master admin repo/org (jefeish-training/safe-settings-config-master), ignoring +2025-09-16T15:47:20.664Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... +2025-09-16T15:47:20.664Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:47:20.966Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:47:21.579Z [DEBUG] Is Admin repo event true +2025-09-16T15:47:21.579Z [DEBUG] Working on the default branch, returning... +2025-09-16T15:47:22.242Z [DEBUG] Branch Protection edited by Bot +2025-09-16T15:47:22.242Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-16T15:47:22.349Z [DEBUG] Not working on the default branch, returning... +2025-09-16T15:47:23.391Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:47:23.391Z [DEBUG] Check run was created! +2025-09-16T15:47:37.290Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:47:37.290Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:47:37.290Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:47:37.812Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:48:45.963Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:48:45.963Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:48:45.963Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:48:46.215Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:50:41.987Z [DEBUG] Check run was created! +2025-09-16T15:50:41.988Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:50:48.685Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:50:48.685Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:50:48.685Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:50:48.941Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:51:28.479Z [DEBUG] Check run was created! +2025-09-16T15:51:28.479Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:51:31.307Z [DEBUG] Check run was created! +2025-09-16T15:51:31.307Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:51:50.461Z [DEBUG] Check run was created! +2025-09-16T15:51:50.461Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:51:51.381Z [DEBUG] Check run was created! +2025-09-16T15:51:51.381Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:35.618Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:58:35.618Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:58:35.618Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:58:36.135Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:58:45.832Z [DEBUG] Check run was created! +2025-09-16T15:58:45.832Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:45.867Z [DEBUG] Check run was created! +2025-09-16T15:58:45.867Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:46.449Z [DEBUG] Is Admin repo event false +2025-09-16T15:58:46.449Z [DEBUG] Not working on the Admin repo, returning... +2025-09-16T15:58:46.724Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:46.724Z [DEBUG] Check run was created! +2025-09-16T15:58:49.218Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:49.218Z [DEBUG] Check run was created! +2025-09-16T16:01:29.319Z [DEBUG] Check run was created! +2025-09-16T16:01:29.319Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:13:27.125Z [DEBUG] Not working on the Admin repo, returning... +2025-09-16T17:13:27.125Z [DEBUG] Is Admin repo event false +2025-09-16T17:13:28.760Z [DEBUG] Check run was created! +2025-09-16T17:13:28.760Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:13:28.848Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:13:28.848Z [DEBUG] Check run was created! +2025-09-16T17:13:28.918Z [DEBUG] Check run was created! +2025-09-16T17:13:28.918Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:14:33.551Z [DEBUG] Check run was created! +2025-09-16T17:14:33.551Z [DEBUG] Not triggered by Safe-settings... +2025-09-18T14:09:14.208Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-18T14:09:14.208Z [INFO] Received 'pull_request.closed' event: 48 +2025-09-18T14:09:14.585Z [INFO] Files changed in PR #48: .github/safe-settings/globals/manifest.yml +2025-09-18T14:09:14.585Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-18T14:09:14.586Z [INFO] Syncing safe settings for 'globals/'. +2025-09-18T14:09:14.924Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-18T14:09:15.216Z [DEBUG] Skipping sync for manifest.yml (should only exist in hub) +2025-09-18T14:09:15.672Z [DEBUG] Check run was created! +2025-09-18T14:09:15.672Z [DEBUG] Not triggered by Safe-settings... +2025-09-18T14:09:23.527Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:09:23.526Z [DEBUG] Is Admin repo event false +2025-09-18T14:13:27.105Z [DEBUG] Is Admin repo event false +2025-09-18T14:13:27.105Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:13:56.822Z [DEBUG] Is Admin repo event false +2025-09-18T14:13:56.822Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:13:56.822Z [DEBUG] Pull_request opened ! +2025-09-18T14:14:06.314Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-18T14:14:06.314Z [INFO] Received 'pull_request.closed' event: 49 +2025-09-18T14:14:07.042Z [INFO] Files changed in PR #49: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:07.043Z [INFO] Syncing safe settings for organization: jefeish-training +2025-09-18T14:14:07.042Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). +2025-09-18T14:14:07.043Z [INFO] Organization: jefeish-training, Destination Repo: safe-settings-config, Destination Folder: .github +2025-09-18T14:14:07.042Z [INFO] Orgs updated in PR #49: jefeish-training +2025-09-18T14:14:07.043Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' +2025-09-18T14:14:07.043Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2025-09-18T14:14:07.333Z [INFO] DEBUG: PR #49 contains 1 changed file(s) +2025-09-18T14:14:07.333Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2025-09-18T14:14:07.333Z [INFO] DEBUG: first file object = { + "sha": "8f345e9e4d6701accc0d39f587d00950c9a17ed5", + "filename": ".github/safe-settings/organizations/jefeish-training/settings.yml", + "status": "modified", + "additions": 175, + "deletions": 175, + "changes": 350, + "blob_url": "https://github.com/jefeish-training/safe-settings-config-master/blob/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml", + "raw_url": "https://github.com/jefeish-training/safe-settings-config-master/raw/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml", + "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml?ref=ee5e72b1fcb62dda5d16fd244fe36bb072589455", + "patch": "@@ -222,178 +222,178 @@ validator:\n \n # Rulesets\n # See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options\n-rulesets:\n- - name: Template\n- # The target of the ruleset. Can be one of:\n- # - branch\n- # - tag\n- target: branch\n- # The enforcement level of the ruleset. `evaluate` allows admins to test\n- # rules before enforcing them.\n- # - disabled\n- # - active\n- # - evaluate\n- enforcement: active\n-\n- # The actors that can bypass the rules in this ruleset\n- bypass_actors:\n- - actor_id: number\n- # type: The type of actor that can bypass a ruleset\n- # - RepositoryRole\n- # - Team\n- # - Integration\n- # - OrganizationAdmin\n- actor_type: Team\n- # When the specified actor can bypass the ruleset. `pull_request`\n- # means that an actor can only bypass rules on pull requests.\n- # - always\n- # - pull_request\n- bypass_mode: pull_request\n-\n- - actor_id: 1\n- actor_type: OrganizationAdmin\n- bypass_mode: always\n-\n- - actor_id: 7898\n- actor_type: RepositoryRole\n- bypass_mode: always\n-\n- - actor_id: 210920\n- actor_type: Integration\n- bypass_mode: always\n-\n- conditions:\n- # Parameters for a repository ruleset ref name condition\n- ref_name:\n- # Array of ref names or patterns to include. One of these\n- # patterns must match for the condition to pass. Also accepts\n- # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n- # include all branches.\n- include: [\"~DEFAULT_BRANCH\"]\n-\n- # Array of ref names or patterns to exclude. The condition\n- # will not pass if any of these patterns match.\n- exclude: [\"refs/heads/oldmaster\"]\n-\n- # This condition only exists at the org level (remove for suborg and repo level rulesets)\n- repository_name:\n- # Array of repository names or patterns to include.\n- # One of these patterns must match for the condition\n- # to pass. Also accepts `~ALL` to include all\n- # repositories.\n- include: [\"test*\"]\n- # Array of repository names or patterns to exclude. The\n- # condition will not pass if any of these patterns\n- # match.\n- exclude: [\"test\", \"test1\"]\n- # Whether renaming of target repositories is\n- # prevented.\n- protected: true\n-\n- # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n- rules:\n- - type: creation\n- - type: update\n- parameters:\n- # Branch can pull changes from its upstream repository\n- update_allows_fetch_and_merge: true\n- - type: deletion\n- - type: required_linear_history\n- - type: required_signatures\n-\n- - type: required_deployments\n- parameters:\n- required_deployment_environments: [\"staging\"]\n-\n- - type: pull_request\n- parameters:\n- # Reviewable commits pushed will dismiss previous pull\n- # request review approvals.\n- dismiss_stale_reviews_on_push: true\n- # Require an approving review in pull requests that modify\n- # files that have a designated code owner\n- require_code_owner_review: true\n- # Whether the most recent reviewable push must be approved\n- # by someone other than the person who pushed it.\n- require_last_push_approval: true\n- # The number of approving reviews that are required before a\n- # pull request can be merged.\n- required_approving_review_count: 1\n- # All conversations on code must be resolved before a pull\n- # request can be merged.\n- required_review_thread_resolution: true\n-\n- # Choose which status checks must pass before branches can be merged\n- # into a branch that matches this rule. When enabled, commits must\n- # first be pushed to another branch, then merged or pushed directly\n- # to a branch that matches this rule after status checks have\n- # passed.\n- - type: required_status_checks\n- parameters:\n- # Whether pull requests targeting a matching branch must be\n- # tested with the latest code. This setting will not take\n- # effect unless at least one status check is enabled.\n- strict_required_status_checks_policy: true\n- required_status_checks:\n- - context: CodeQL\n- integration_id: 1234\n- - context: GHAS Compliance\n- integration_id: 1234\n-\n- # Choose which workflows must pass before branches can be merged.\n- - type: workflows\n- parameters:\n- workflows:\n- - path: .github/workflows/example.yml\n- # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n- # in the browser console of the repository to get the repository_id\n- repository_id: 123456\n- # One of the following:\n- # Branch or tag\n- ref: refs/heads/main\n- # Commit SHA\n- sha: 1234567890abcdef\n-\n- - type: commit_message_pattern\n- parameters:\n- name: test commit_message_pattern\n- # required:\n- # - operator\n- # - pattern\n- negate: true\n- operator: starts_with\n- # The operator to use for matching.\n- # - starts_with\n- # - ends_with\n- # - contains\n- # - regex\n- pattern: skip*\n- # The pattern to match with.\n-\n- - type: commit_author_email_pattern\n- parameters:\n- name: test commit_author_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: committer_email_pattern\n- parameters:\n- name: test committer_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: branch_name_pattern\n- parameters:\n- name: test branch_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n-\n- - type: \"tag_name_pattern\"\n- parameters:\n- name: test tag_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n+# rulesets:\n+# - name: Template\n+# # The target of the ruleset. Can be one of:\n+# # - branch\n+# # - tag\n+# target: branch\n+# # The enforcement level of the ruleset. `evaluate` allows admins to test\n+# # rules before enforcing them.\n+# # - disabled\n+# # - active\n+# # - evaluate\n+# enforcement: active\n+\n+# # The actors that can bypass the rules in this ruleset\n+# bypass_actors:\n+# - actor_id: number\n+# # type: The type of actor that can bypass a ruleset\n+# # - RepositoryRole\n+# # - Team\n+# # - Integration\n+# # - OrganizationAdmin\n+# actor_type: Team\n+# # When the specified actor can bypass the ruleset. `pull_request`\n+# # means that an actor can only bypass rules on pull requests.\n+# # - always\n+# # - pull_request\n+# bypass_mode: pull_request\n+\n+# - actor_id: 1\n+# actor_type: OrganizationAdmin\n+# bypass_mode: always\n+\n+# - actor_id: 7898\n+# actor_type: RepositoryRole\n+# bypass_mode: always\n+\n+# - actor_id: 210920\n+# actor_type: Integration\n+# bypass_mode: always\n+\n+# conditions:\n+# # Parameters for a repository ruleset ref name condition\n+# ref_name:\n+# # Array of ref names or patterns to include. One of these\n+# # patterns must match for the condition to pass. Also accepts\n+# # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n+# # include all branches.\n+# include: [\"~DEFAULT_BRANCH\"]\n+\n+# # Array of ref names or patterns to exclude. The condition\n+# # will not pass if any of these patterns match.\n+# exclude: [\"refs/heads/oldmaster\"]\n+\n+# # This condition only exists at the org level (remove for suborg and repo level rulesets)\n+# repository_name:\n+# # Array of repository names or patterns to include.\n+# # One of these patterns must match for the condition\n+# # to pass. Also accepts `~ALL` to include all\n+# # repositories.\n+# include: [\"test*\"]\n+# # Array of repository names or patterns to exclude. The\n+# # condition will not pass if any of these patterns\n+# # match.\n+# exclude: [\"test\", \"test1\"]\n+# # Whether renaming of target repositories is\n+# # prevented.\n+# protected: true\n+\n+# # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n+# rules:\n+# - type: creation\n+# - type: update\n+# parameters:\n+# # Branch can pull changes from its upstream repository\n+# update_allows_fetch_and_merge: true\n+# - type: deletion\n+# - type: required_linear_history\n+# - type: required_signatures\n+\n+# - type: required_deployments\n+# parameters:\n+# required_deployment_environments: [\"staging\"]\n+\n+# - type: pull_request\n+# parameters:\n+# # Reviewable commits pushed will dismiss previous pull\n+# # request review approvals.\n+# dismiss_stale_reviews_on_push: true\n+# # Require an approving review in pull requests that modify\n+# # files that have a designated code owner\n+# require_code_owner_review: true\n+# # Whether the most recent reviewable push must be approved\n+# # by someone other than the person who pushed it.\n+# require_last_push_approval: true\n+# # The number of approving reviews that are required before a\n+# # pull request can be merged.\n+# required_approving_review_count: 1\n+# # All conversations on code must be resolved before a pull\n+# # request can be merged.\n+# required_review_thread_resolution: true\n+\n+# # Choose which status checks must pass before branches can be merged\n+# # into a branch that matches this rule. When enabled, commits must\n+# # first be pushed to another branch, then merged or pushed directly\n+# # to a branch that matches this rule after status checks have\n+# # passed.\n+# - type: required_status_checks\n+# parameters:\n+# # Whether pull requests targeting a matching branch must be\n+# # tested with the latest code. This setting will not take\n+# # effect unless at least one status check is enabled.\n+# strict_required_status_checks_policy: true\n+# required_status_checks:\n+# - context: CodeQL\n+# integration_id: 1234\n+# - context: GHAS Compliance\n+# integration_id: 1234\n+\n+# # Choose which workflows must pass before branches can be merged.\n+# - type: workflows\n+# parameters:\n+# workflows:\n+# - path: .github/workflows/example.yml\n+# # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n+# # in the browser console of the repository to get the repository_id\n+# repository_id: 123456\n+# # One of the following:\n+# # Branch or tag\n+# ref: refs/heads/main\n+# # Commit SHA\n+# sha: 1234567890abcdef\n+\n+# - type: commit_message_pattern\n+# parameters:\n+# name: test commit_message_pattern\n+# # required:\n+# # - operator\n+# # - pattern\n+# negate: true\n+# operator: starts_with\n+# # The operator to use for matching.\n+# # - starts_with\n+# # - ends_with\n+# # - contains\n+# # - regex\n+# pattern: skip*\n+# # The pattern to match with.\n+\n+# - type: commit_author_email_pattern\n+# parameters:\n+# name: test commit_author_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: committer_email_pattern\n+# parameters:\n+# name: test committer_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: branch_name_pattern\n+# parameters:\n+# name: test branch_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"\n+\n+# - type: \"tag_name_pattern\"\n+# parameters:\n+# name: test tag_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"" +} +2025-09-18T14:14:07.333Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:07.334Z [INFO] DEBUG: FILE[0] raw={"sha":"8f345e9e4d6701accc0d39f587d00950c9a17ed5","filename":".github/safe-settings/organizations/jefeish-training/settings.yml","status":"modified","additions":175,"deletions":175,"changes":350,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml?ref=ee5e72b1fcb62dda5d16fd244fe36bb072589455","patch":"@@ -222,178 +222,178 @@ validator:\n \n # Rulesets\n # See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options\n-rulesets:\n- - name: Template\n- # The target of the ruleset. Can be one of:\n- # - branch\n- # - tag\n- target: branch\n- # The enforcement level of the ruleset. `evaluate` allows admins to test\n- # rules before enforcing them.\n- # - disabled\n- # - active\n- # - evaluate\n- enforcement: active\n-\n- # The actors that can bypass the rules in this ruleset\n- bypass_actors:\n- - actor_id: number\n- # type: The type of actor that can bypass a ruleset\n- # - RepositoryRole\n- # - Team\n- # - Integration\n- # - OrganizationAdmin\n- actor_type: Team\n- # When the specified actor can bypass the ruleset. `pull_request`\n- # means that an actor can only bypass rules on pull requests.\n- # - always\n- # - pull_request\n- bypass_mode: pull_request\n-\n- - actor_id: 1\n- actor_type: OrganizationAdmin\n- bypass_mode: always\n-\n- - actor_id: 7898\n- actor_type: RepositoryRole\n- bypass_mode: always\n-\n- - actor_id: 210920\n- actor_type: Integration\n- bypass_mode: always\n-\n- conditions:\n- # Parameters for a repository ruleset ref name condition\n- ref_name:\n- # Array of ref names or patterns to include. One of these\n- # patterns must match for the condition to pass. Also accepts\n- # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n- # include all branches.\n- include: [\"~DEFAULT_BRANCH\"]\n-\n- # Array of ref names or patterns to exclude. The condition\n- # will not pass if any of these patterns match.\n- exclude: [\"refs/heads/oldmaster\"]\n-\n- # This condition only exists at the org level (remove for suborg and repo level rulesets)\n- repository_name:\n- # Array of repository names or patterns to include.\n- # One of these patterns must match for the condition\n- # to pass. Also accepts `~ALL` to include all\n- # repositories.\n- include: [\"test*\"]\n- # Array of repository names or patterns to exclude. The\n- # condition will not pass if any of these patterns\n- # match.\n- exclude: [\"test\", \"test1\"]\n- # Whether renaming of target repositories is\n- # prevented.\n- protected: true\n-\n- # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n- rules:\n- - type: creation\n- - type: update\n- parameters:\n- # Branch can pull changes from its upstream repository\n- update_allows_fetch_and_merge: true\n- - type: deletion\n- - type: required_linear_history\n- - type: required_signatures\n-\n- - type: required_deployments\n- parameters:\n- required_deployment_environments: [\"staging\"]\n-\n- - type: pull_request\n- parameters:\n- # Reviewable commits pushed will dismiss previous pull\n- # request review approvals.\n- dismiss_stale_reviews_on_push: true\n- # Require an approving review in pull requests that modify\n- # files that have a designated code owner\n- require_code_owner_review: true\n- # Whether the most recent reviewable push must be approved\n- # by someone other than the person who pushed it.\n- require_last_push_approval: true\n- # The number of approving reviews that are required before a\n- # pull request can be merged.\n- required_approving_review_count: 1\n- # All conversations on code must be resolved before a pull\n- # request can be merged.\n- required_review_thread_resolution: true\n-\n- # Choose which status checks must pass before branches can be merged\n- # into a branch that matches this rule. When enabled, commits must\n- # first be pushed to another branch, then merged or pushed directly\n- # to a branch that matches this rule after status checks have\n- # passed.\n- - type: required_status_checks\n- parameters:\n- # Whether pull requests targeting a matching branch must be\n- # tested with the latest code. This setting will not take\n- # effect unless at least one status check is enabled.\n- strict_required_status_checks_policy: true\n- required_status_checks:\n- - context: CodeQL\n- integration_id: 1234\n- - context: GHAS Compliance\n- integration_id: 1234\n-\n- # Choose which workflows must pass before branches can be merged.\n- - type: workflows\n- parameters:\n- workflows:\n- - path: .github/workflows/example.yml\n- # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n- # in the browser console of the repository to get the repository_id\n- repository_id: 123456\n- # One of the following:\n- # Branch or tag\n- ref: refs/heads/main\n- # Commit SHA\n- sha: 1234567890abcdef\n-\n- - type: commit_message_pattern\n- parameters:\n- name: test commit_message_pattern\n- # required:\n- # - operator\n- # - pattern\n- negate: true\n- operator: starts_with\n- # The operator to use for matching.\n- # - starts_with\n- # - ends_with\n- # - contains\n- # - regex\n- pattern: skip*\n- # The pattern to match with.\n-\n- - type: commit_author_email_pattern\n- parameters:\n- name: test commit_author_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: committer_email_pattern\n- parameters:\n- name: test committer_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: branch_name_pattern\n- parameters:\n- name: test branch_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n-\n- - type: \"tag_name_pattern\"\n- parameters:\n- name: test tag_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n+# rulesets:\n+# - name: Template\n+# # The target of the ruleset. Can be one of:\n+# # - branch\n+# # - tag\n+# target: branch\n+# # The enforcement level of the ruleset. `evaluate` allows admins to test\n+# # rules before enforcing them.\n+# # - disabled\n+# # - active\n+# # - evaluate\n+# enforcement: active\n+\n+# # The actors that can bypass the rules in this ruleset\n+# bypass_actors:\n+# - actor_id: number\n+# # type: The type of actor that can bypass a ruleset\n+# # - RepositoryRole\n+# # - Team\n+# # - Integration\n+# # - OrganizationAdmin\n+# actor_type: Team\n+# # When the specified actor can bypass the ruleset. `pull_request`\n+# # means that an actor can only bypass rules on pull requests.\n+# # - always\n+# # - pull_request\n+# bypass_mode: pull_request\n+\n+# - actor_id: 1\n+# actor_type: OrganizationAdmin\n+# bypass_mode: always\n+\n+# - actor_id: 7898\n+# actor_type: RepositoryRole\n+# bypass_mode: always\n+\n+# - actor_id: 210920\n+# actor_type: Integration\n+# bypass_mode: always\n+\n+# conditions:\n+# # Parameters for a repository ruleset ref name condition\n+# ref_name:\n+# # Array of ref names or patterns to include. One of these\n+# # patterns must match for the condition to pass. Also accepts\n+# # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n+# # include all branches.\n+# include: [\"~DEFAULT_BRANCH\"]\n+\n+# # Array of ref names or patterns to exclude. The condition\n+# # will not pass if any of these patterns match.\n+# exclude: [\"refs/heads/oldmaster\"]\n+\n+# # This condition only exists at the org level (remove for suborg and repo level rulesets)\n+# repository_name:\n+# # Array of repository names or patterns to include.\n+# # One of these patterns must match for the condition\n+# # to pass. Also accepts `~ALL` to include all\n+# # repositories.\n+# include: [\"test*\"]\n+# # Array of repository names or patterns to exclude. The\n+# # condition will not pass if any of these patterns\n+# # match.\n+# exclude: [\"test\", \"test1\"]\n+# # Whether renaming of target repositories is\n+# # prevented.\n+# protected: true\n+\n+# # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n+# rules:\n+# - type: creation\n+# - type: update\n+# parameters:\n+# # Branch can pull changes from its upstream repository\n+# update_allows_fetch_and_merge: true\n+# - type: deletion\n+# - type: required_linear_history\n+# - type: required_signatures\n+\n+# - type: required_deployments\n+# parameters:\n+# required_deployment_environments: [\"staging\"]\n+\n+# - type: pull_request\n+# parameters:\n+# # Reviewable commits pushed will dismiss previous pull\n+# # request review approvals.\n+# dismiss_stale_reviews_on_push: true\n+# # Require an approving review in pull requests that modify\n+# # files that have a designated code owner\n+# require_code_owner_review: true\n+# # Whether the most recent reviewable push must be approved\n+# # by someone other than the person who pushed it.\n+# require_last_push_approval: true\n+# # The number of approving reviews that are required before a\n+# # pull request can be merged.\n+# required_approving_review_count: 1\n+# # All conversations on code must be resolved before a pull\n+# # request can be merged.\n+# required_review_thread_resolution: true\n+\n+# # Choose which status checks must pass before branches can be merged\n+# # into a branch that matches this rule. When enabled, commits must\n+# # first be pushed to another branch, then merged or pushed directly\n+# # to a branch that matches this rule after status checks have\n+# # passed.\n+# - type: required_status_checks\n+# parameters:\n+# # Whether pull requests targeting a matching branch must be\n+# # tested with the latest code. This setting will not take\n+# # effect unless at least one status check is enabled.\n+# strict_required_status_checks_policy: true\n+# required_status_checks:\n+# - context: CodeQL\n+# integration_id: 1234\n+# - context: GHAS Compliance\n+# integration_id: 1234\n+\n+# # Choose which workflows must pass before branches can be merged.\n+# - type: workflows\n+# parameters:\n+# workflows:\n+# - path: .github/workflows/example.yml\n+# # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n+# # in the browser console of the repository to get the repository_id\n+# repository_id: 123456\n+# # One of the following:\n+# # Branch or tag\n+# ref: refs/heads/main\n+# # Commit SHA\n+# sha: 1234567890abcdef\n+\n+# - type: commit_message_pattern\n+# parameters:\n+# name: test commit_message_pattern\n+# # required:\n+# # - operator\n+# # - pattern\n+# negate: true\n+# operator: starts_with\n+# # The operator to use for matching.\n+# # - starts_with\n+# # - ends_with\n+# # - contains\n+# # - regex\n+# pattern: skip*\n+# # The pattern to match with.\n+\n+# - type: commit_author_email_pattern\n+# parameters:\n+# name: test commit_author_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: committer_email_pattern\n+# parameters:\n+# name: test committer_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: branch_name_pattern\n+# parameters:\n+# name: test branch_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"\n+\n+# - type: \"tag_name_pattern\"\n+# parameters:\n+# name: test tag_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\""} +2025-09-18T14:14:07.334Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-training/settings.yml" length=65 +2025-09-18T14:14:07.334Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:07.334Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-training +2025-09-18T14:14:07.335Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-training +2025-09-18T14:14:07.536Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #49 to jefeish-training/safe-settings-config@main under .github (directPush=true) +2025-09-18T14:14:09.113Z [INFO] Committed .github/settings.yml to jefeish-training/safe-settings-config@main +2025-09-18T14:14:09.113Z [INFO] Changes pushed directly to jefeish-training/safe-settings-config@main +2025-09-18T14:14:10.074Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... +2025-09-18T14:14:10.074Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-18T14:14:10.392Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-18T14:14:12.340Z [DEBUG] Is Admin repo event true +2025-09-18T14:14:12.340Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:14:49.484Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-18T14:14:51.219Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-18T14:14:51.219Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-18T14:14:51.219Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-18T14:14:51.485Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:51.485Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:14:51.485Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-18T14:14:51.745Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:14:51.745Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-18T14:14:52.466Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-18T14:14:52.465Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-18T14:14:52.466Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-18T14:14:52.707Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:14:52.707Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-18T14:14:52.707Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:52.991Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:14:52.992Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-18T14:14:53.724Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-18T14:14:53.724Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-18T14:14:53.724Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-18T14:14:53.987Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:53.987Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-18T14:14:53.987Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:14:54.285Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:14:54.285Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:54.869Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-18T14:14:54.869Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:14:54.869Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-18T14:14:54.869Z [DEBUG] 9. [SYNC DEBUG] Org hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:14:55.440Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:14:55.440Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-18T14:14:55.440Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:14:56.225Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-18T14:14:56.225Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-18T14:14:56.225Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-18T14:14:56.469Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:14:56.469Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:56.469Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-18T14:14:56.735Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:14:56.735Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-18T14:15:07.249Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-18T14:15:08.957Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-18T14:15:08.957Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-18T14:15:08.957Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-18T14:15:09.220Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-18T14:15:09.220Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:15:09.220Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:09.484Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-18T14:15:09.484Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:15:10.150Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-18T14:15:10.150Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-18T14:15:10.150Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-18T14:15:10.394Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:15:10.394Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-18T14:15:10.394Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:10.648Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:15:10.648Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-18T14:15:11.505Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-18T14:15:11.505Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-18T14:15:11.505Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-18T14:15:11.761Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:11.761Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:15:11.761Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-18T14:15:12.100Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:15:12.100Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:15:13.786Z [DEBUG] 9. [SYNC DEBUG] Org hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:15:13.786Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-18T14:15:13.786Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:15:13.786Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-18T14:15:14.195Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:15:14.195Z [DEBUG] Is Admin repo event false +2025-09-18T14:15:14.354Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:15:14.354Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-18T14:15:14.354Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:15:15.049Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-18T14:15:15.049Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-18T14:15:15.049Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-18T14:15:15.316Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:15:15.316Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:15.316Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-18T14:15:15.620Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:15:15.620Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-18T14:21:33.248Z [INFO] Received 'pull_request.closed' event: 50 +2025-09-18T14:21:33.248Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-18T14:21:34.144Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:21:34.144Z [DEBUG] Is Admin repo event false +2025-09-18T14:21:34.212Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-18T14:21:34.212Z [INFO] Files changed in PR #50: .github/safe-settings/globals/repo.yml +2025-09-18T14:21:34.212Z [INFO] Syncing safe settings for 'globals/'. +2025-09-18T14:21:34.524Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-18T14:21:34.737Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/repo.yml +2025-09-18T14:21:34.737Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-18T14:21:34.737Z [DEBUG] Rule 'global-defaults' matches file 'repo.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-18T14:21:35.425Z [DEBUG] Checking existence of .github/repo.yml in jetest99/safe-settings-config +2025-09-18T14:21:35.732Z [INFO] File .github/repo.yml not found in jetest99/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:35.733Z [INFO] Syncing repo.yml to jetest99 (mergeStrategy=merge) +2025-09-18T14:21:36.704Z [INFO] Committed .github/repo.yml to jetest99/safe-settings-config@main +2025-09-18T14:21:36.704Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-18T14:21:36.704Z [INFO] Changes pushed directly to jetest99/safe-settings-config@main +2025-09-18T14:21:37.083Z [DEBUG] Checking existence of .github/repo.yml in jefeish-training/safe-settings-config +2025-09-18T14:21:37.357Z [INFO] File .github/repo.yml not found in jefeish-training/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:37.357Z [INFO] Syncing repo.yml to jefeish-training (mergeStrategy=merge) +2025-09-18T14:21:37.740Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:38.344Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-18T14:21:38.344Z [INFO] Changes pushed directly to jefeish-training/safe-settings-config@main +2025-09-18T14:21:38.343Z [INFO] Committed .github/repo.yml to jefeish-training/safe-settings-config@main +2025-09-18T14:21:38.554Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:38.554Z [DEBUG] Is Admin repo event true +2025-09-18T14:21:39.149Z [DEBUG] Checking existence of .github/repo.yml in jefeish-test1/safe-settings-config +2025-09-18T14:21:39.418Z [INFO] File .github/repo.yml not found in jefeish-test1/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:39.418Z [INFO] Syncing repo.yml to jefeish-test1 (mergeStrategy=merge) +2025-09-18T14:21:39.482Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:40.470Z [INFO] Committed .github/repo.yml to jefeish-test1/safe-settings-config@main +2025-09-18T14:21:40.471Z [INFO] Changes pushed directly to jefeish-test1/safe-settings-config@main +2025-09-18T14:21:40.471Z [DEBUG] Preparing to sync file 'repo.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-18T14:21:40.690Z [DEBUG] Is Admin repo event true +2025-09-18T14:21:40.690Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:40.962Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-18T14:21:40.961Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-18T14:21:41.228Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:41.575Z [DEBUG] Checking existence of .github/repo.yml in jefeish-migration-test/safe-settings-config +2025-09-18T14:21:41.832Z [INFO] File .github/repo.yml not found in jefeish-migration-test/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:41.832Z [INFO] Syncing repo.yml to jefeish-migration-test (mergeStrategy=merge) +2025-09-18T14:21:42.191Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:42.191Z [DEBUG] Is Admin repo event true +2025-09-18T14:21:42.736Z [INFO] Committed .github/repo.yml to jefeish-migration-test/safe-settings-config@main +2025-09-18T14:21:42.736Z [DEBUG] Preparing to sync file 'repo.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-18T14:21:42.736Z [INFO] Changes pushed directly to jefeish-migration-test/safe-settings-config@main +2025-09-18T14:21:43.197Z [DEBUG] Preparing to sync file 'repo.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-18T14:21:43.197Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-18T14:21:43.647Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. +2025-09-18T14:21:43.751Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:54.154Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:54.154Z [DEBUG] Is Admin repo event true diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js index 8ef694afe..24d5b5496 100644 --- a/lib/hubSyncHandler.js +++ b/lib/hubSyncHandler.js @@ -1,5 +1,62 @@ +const { minimatch } = require('minimatch') const env = require('./env') const { getInstallations } = require('./installationCache') +const yaml = require('js-yaml') +const path = require('path') +const fs = require('fs') +const os = require('os') +const util = require('util') + +/** + * Attach a file-backed logger to robot.log that mirrors all log calls to a file. + * It preserves the original behavior and appends each log line to a file, trimming + * the file to the last `maxLines` entries (default 1000). + * + * Usage: call attachFileLogger(robot, { filePath: '/tmp/safe-settings.log', maxLines: 1000 }) + */ +function attachFileLogger (robot, options = {}) { + if (!robot || !robot.log) return + if (robot.log.__fileLoggerAttached) return + const filePath = options.filePath || process.env.SAFE_SETTINGS_LOG_FILE || path.join(process.cwd(), 'hubSyncHandler.log') + const maxLines = Number(options.maxLines || process.env.SAFE_SETTINGS_LOG_FILE_MAX_LINES || 1000) + const methods = ['info', 'warn', 'debug', 'error', 'fatal', 'trace', 'notice'] + + methods.forEach(method => { + const orig = (robot.log && robot.log[method]) ? robot.log[method].bind(robot.log) : (...args) => { /* no-op */ } + robot.log[method] = (...args) => { + // call original logger so console output still occurs + try { orig(...args) } catch (e) { /* swallow */ } + + // Build a single-line message representation + try { + const msg = args.map(a => (typeof a === 'string' ? a : util.inspect(a, { depth: 2 }))).join(' ') + const line = `${new Date().toISOString()} [${method.toUpperCase()}] ${msg}` + // append and then trim to last `maxLines` + fs.appendFile(filePath, line + os.EOL, err => { + if (err) { + try { orig(`Failed to append log to ${filePath}: ${err.message}`) } catch (e) { /* swallow */ } + return + } + // trim asynchronously + fs.promises.readFile(filePath, 'utf8').then(data => { + const lines = data.split(/\r?\n/) + // Remove a possible trailing empty line created by join + if (lines.length && lines[lines.length - 1] === '') lines.pop() + if (lines.length > maxLines) { + const tail = lines.slice(-maxLines) + return fs.promises.writeFile(filePath, tail.join(os.EOL) + os.EOL, 'utf8') + } + return Promise.resolve() + }).catch(() => { /* don't break logging on trim failures */ }) + }) + } catch (e) { + try { orig(`Failed to write log to ${filePath}: ${e && e.message ? e.message : e}`) } catch (e) { /* swallow */ } + } + } + }) + + robot.log.__fileLoggerAttached = true +} /** * Get authenticated octokit client for an org installation @@ -16,6 +73,36 @@ async function getOrgInstallation (robot, orgName) { return await robot.auth(install.id) } + +// Helper to create a branch if not direct push +async function createBranchIfNeeded(githubClient, owner, repo, baseBranch, branchName, directPush, logger) { + if (!directPush) { + try { + const baseRef = await githubClient.rest.git.getRef({ owner, repo, ref: `heads/${baseBranch}` }) + const baseSha = baseRef.data.object.sha + await githubClient.rest.git.createRef({ owner, repo, ref: `refs/heads/${branchName}`, sha: baseSha }) + logger.info(`Created branch ${branchName} in ${owner}/${repo}`) + } catch (err) { + if (err.status === 422) { + logger.warn(`Branch ${branchName} already exists, continuing`) + } else { + throw err + } + } + } +} + +// Helper to create or update a file in a repo +async function createOrUpdateFile(githubClient, params, logger) { + try { + await githubClient.rest.repos.createOrUpdateFileContents(params) + logger.info(`Committed ${params.path} to ${params.owner}/${params.repo}@${params.branch}`) + } catch (err) { + logger.error(`Failed to sync file ${params.path}: ${err.message}`) + throw err + } +} + /** * Sync changed safe-settings organization files from the master admin PR * into the target organization's admin repository. @@ -26,9 +113,9 @@ async function getOrgInstallation (robot, orgName) { * @param {string} destinationFolder Base folder in destination repo where content lives (e.g. .github or .github/safe-settings) */ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationFolder) { + attachFileLogger(robot) try { robot.log.info(`Syncing safe settings for organization: ${orgName}`) - robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`) const pr = context.payload.pull_request if (!pr) { @@ -37,27 +124,16 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF } const { owner: srcOwner, repo: srcRepo } = context.repo() const pull_number = pr.number - - // Source base path where org folders live inside master admin repo - - // 'safe-settings' is the standard sub-folder path const configRoot = env.CONFIG_PATH || '.github/' const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, '') robot.log.info(`DEBUG: sourceBase='${sourceBase}'`) - - // Debug info: log env and computed paths robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`) - - // List changed files in PR const files = await context.octokit.paginate( context.octokit.rest.pulls.listFiles, { owner: srcOwner, repo: srcRepo, pull_number, per_page: 100 } ) - robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`) if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) - - // Dump file objects for debugging filename issues if (files.length) { try { robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`) @@ -74,7 +150,6 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF } }) } - const orgPrefix = `${sourceBase}/${orgName}/` robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`) @@ -82,13 +157,11 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`) if (!relevant.length) { robot.log.info(`No files for org ${orgName} in PR #${pull_number}`) - // Detailed per-file checks to help debug matching files.forEach(f => { const exact = f.filename === `${sourceBase}/${orgName}` const pref = f.filename.startsWith(orgPrefix) robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`) }) - // Also show alternate check using CONFIG_PATH + '/organizations' const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations` const altPrefix = `${altBase}/${orgName}/` files.forEach(f => { @@ -98,88 +171,53 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF }) return } - - // Destination info const destOwner = orgName - // ensure destBase uses the configured CONFIG_PATH (fallback to '.github') and normalize trailing slash const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, '') const destBaseBranch = 'main' const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') - - // Find installation for destination org to auth (reusable helper) const githubDest = await getOrgInstallation(robot, destOwner) if (!githubDest) { robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`) return } - robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`) - - // Create branch if not direct push const timestamp = Date.now() const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}` - if (!directPush) { - try { - const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }) - const baseSha = baseRef.data.object.sha - await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) - robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`) - } catch (err) { - if (err.status === 422) { - robot.log.warn(`Branch ${branchName} already exists, continuing`) - } else { - throw err - } - } - } - + await createBranchIfNeeded(githubDest, destOwner, destRepo, destBaseBranch, branchName, directPush, robot.log) for (const f of relevant) { let relative if (f.filename === `${sourceBase}/${orgName}`) { - // top directory marker encountered (unlikely in changed files list) - skip continue } else { relative = f.filename.slice(orgPrefix.length) } - // place only the changed file under the configured CONFIG_PATH (e.g. '.github/') const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/') + const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }) + const data = srcContentResp.data + if (Array.isArray(data)) { + continue + } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') + const encoded = Buffer.from(fileContent, 'utf8').toString('base64') + let existingSha try { - const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }) - const data = srcContentResp.data - if (Array.isArray(data)) { - // Skip directories; individual files will appear separately in changed files list - continue - } - const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') - const encoded = Buffer.from(fileContent, 'utf8').toString('base64') - - // Check existing file for sha - let existingSha - try { - const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }) - if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha - } catch (getErr) { - if (getErr.status !== 404) throw getErr // ignore missing - } - - await githubDest.rest.repos.createOrUpdateFileContents({ - owner: destOwner, - repo: destRepo, - path: destPath, - message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, - content: encoded, - branch: branchName, - sha: existingSha, - committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, - author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } - }) - robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`) - } catch (fileErr) { - robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`) - throw fileErr + const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }) + if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha + } catch (getErr) { + if (getErr.status !== 404) throw getErr } + await createOrUpdateFile(githubDest, { + owner: destOwner, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, + content: encoded, + branch: branchName, + sha: existingSha, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }, robot.log) } - if (!directPush) { try { const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` @@ -205,6 +243,7 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF * @param {import('probot').Context} context */ async function hubSyncHandler (robot, context) { + attachFileLogger(robot) const { payload } = context const { repository, pull_request } = payload || {} robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`) @@ -238,12 +277,12 @@ async function hubSyncHandler (robot, context) { const orgsChanged = files.some(f => /\/organizations\//.test(f.filename)) if (globalsChanged) { - robot.log.info('Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...).') + robot.log.debug('Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...).') await module.exports.syncHubGlobalsUpdate(robot, context, files) } if (orgsChanged) { - robot.log.info('Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...).') + robot.log.debug('Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...).') // Only sync updates in organization subfolders, not files directly in organizations folder const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations` const normalizedBase = baseSettingsPath.replace(/\/$/, '') @@ -277,210 +316,166 @@ async function hubSyncHandler (robot, context) { * @param {Array} files - Array of changed file objects from PR */ async function syncHubGlobalsUpdate (robot, context, files) { - robot.log.info('syncHubGlobalsUpdate: Processing globals folder changes.') - // Step 1: Load manifest.yml rules from the hub repo - const yaml = require('js-yaml') - const util = require('util') + attachFileLogger(robot) + robot.log.info(`Syncing safe settings for 'globals/'.`) const manifestPath = `${env.CONFIG_PATH}/${env.SAFE_SETTINGS_HUB_PATH}/globals/manifest.yml` let manifest try { - // Get manifest.yml from the hub repo (default branch: main) const resp = await context.octokit.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: manifestPath, ref: 'main' }) - const manifestContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') manifest = yaml.load(manifestContent) - robot.log.info('Loaded manifest.yml rules from hub repo:' + JSON.stringify(manifest, null, 2)) + robot.log.debug('Loaded manifest.yml rules from hub repo:' + JSON.stringify(manifest, null, 2)) } catch (err) { robot.log.error('Failed to load manifest.yml from hub repo:' + err.message) return } - // Step 2: Determine which update to sync where - // Find changed files in the globals folder const changedGlobals = files.filter(f => /\/globals\//.test(f.filename)) if (!changedGlobals.length) { robot.log.info('No changed files in globals folder.') return } - - // For each changed file, match against manifest rules + // Pre-filter rules for each file, and precompute orgs for each rule + const installs = await getInstallations(robot) + const orgLogins = installs.filter(i => i.account && i.account.type === 'Organization').map(i => i.account.login) + // Precompute matching rules for each fileName in changedGlobals + const fileNameToMatchingRules = {}; + for (const fileObj of changedGlobals) { + const fileName = fileObj.filename.split('/').pop(); + fileNameToMatchingRules[fileName] = (manifest.rules || []).filter(rule => + (rule.files || []).some(pattern => minimatch(fileName, pattern)) + ); + } for (const fileObj of changedGlobals) { - const fileName = fileObj.filename.split('/').pop() - // Prevent manifest.yml from being synced to organizations + const fileName = fileObj.filename.split('/').pop(); if (fileName === 'manifest.yml') { - robot.log.info(`Skipping sync for manifest.yml (should only exist in hub)`) - continue + robot.log.debug(`Skipping sync for manifest.yml (should only exist in hub)`); + continue; } - robot.log.info(`Evaluating globals file: ${fileObj.filename}`) - for (const rule of manifest.rules || []) { - // Check if file matches rule.files (glob match, simple * and exact) - const matchesFile = (rule.files || []).some(pattern => { - if (pattern === fileName) return true - if (pattern.startsWith('*') && fileName.endsWith(pattern.slice(1))) return true - if (pattern.endsWith('*') && fileName.startsWith(pattern.slice(0, -1))) return true - if (pattern.includes('*')) { - // Simple contains match for * - const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') - return regex.test(fileName) - } - return false - }) - if (!matchesFile) continue - - // Determine target orgs - const targets = rule.targets || [] - robot.log.info(`Rule '${rule.name}' matches file '${fileName}'. Targets: ${targets.join(', ')}`) - // Step 3: handle mergeStrategy and actual sync - const mergeStrategy = rule.mergeStrategy || 'merge' - for (const orgPattern of targets) { - // For demo, treat '*' as all orgs, otherwise match orgs by pattern - let orgsToSync = [] + robot.log.debug(`Evaluating globals file: ${fileObj.filename}`); + // Use precomputed matching rules + const matchingRules = fileNameToMatchingRules[fileName]; + for (const rule of matchingRules) { + const mergeStrategy = rule.mergeStrategy || 'merge'; + // Precompute orgs to sync for each target pattern + let orgsToSync = []; + for (const orgPattern of rule.targets || []) { if (orgPattern === '*') { - // Get all org installations - const installs = await getInstallations(robot) - orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization').map(i => i.account.login) + orgsToSync.push(...orgLogins); } else if (orgPattern.endsWith('*')) { - // Prefix match - const prefix = orgPattern.slice(0, -1) - const installs = await getInstallations(robot) - orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization' && i.account.login.startsWith(prefix)).map(i => i.account.login) + const prefix = orgPattern.slice(0, -1); + orgsToSync.push(...orgLogins.filter(login => login.startsWith(prefix))); } else { - orgsToSync = [orgPattern] + orgsToSync.push(orgPattern); } - for (const orgName of orgsToSync) { - robot.log.info(`Preparing to sync file '${fileName}' to org '${orgName}' with mergeStrategy='${mergeStrategy}'`) - // Check if org has a safe-settings config repo (repo exists) - const destRepo = env.ADMIN_REPO - // use the octokit client authenticated for the hub installation - const githubDest = await getOrgInstallation(robot, orgName) - if (!githubDest) { - robot.log.info(`Skipping org ${orgName}: no installation found.`) - continue + } + // Remove duplicates + orgsToSync = Array.from(new Set(orgsToSync)); + robot.log.debug(`Rule '${rule.name}' matches file '${fileName}'. Targets: ${orgsToSync.join(', ')}`); + for (const orgName of orgsToSync) { + robot.log.debug(`Preparing to sync file '${fileName}' to org '${orgName}' with mergeStrategy='${mergeStrategy}'`); + const destRepo = env.ADMIN_REPO; + const githubDest = await getOrgInstallation(robot, orgName); + if (!githubDest) { + robot.log.info(`Skipping org ${orgName}: no installation found.`); + continue; + } + let repoExists = false; + try { + await githubDest.repos.get({ owner: orgName, repo: destRepo }); + repoExists = true; + } catch (err) { + if (err.status === 404) { + robot.log.info(`Skipping org ${orgName}: config repo '${destRepo}' does not exist.`); + continue; + } else { + throw err; } - let repoExists = false - try { - await githubDest.repos.get({ owner: orgName, repo: destRepo }) - repoExists = true - } catch (err) { - if (err.status === 404) { - robot.log.info(`Skipping org ${orgName}: config repo '${destRepo}' does not exist.`) - continue - } else { - throw err - } + } + if (!repoExists) continue; + const destPath = `${env.CONFIG_PATH}/${fileName}`; + let exists = false; + let existingSha = undefined; + try { + robot.log.debug(`Checking existence of ${destPath} in ${orgName}/${destRepo}`); + const resp = await githubDest.repos.getContent({ + owner: orgName, + repo: destRepo, + path: destPath, + ref: 'main' + }); + if (!Array.isArray(resp.data)) { + robot.log.debug(`Found ${destPath} in ${orgName}/${destRepo}`); + exists = true; + existingSha = resp.data.sha; } - if (!repoExists) continue - // Check if file exists in org's repo - const destPath = `${env.CONFIG_PATH}/${fileName}` - let exists = false - let existingContent = null - try { - robot.log.info(`Checking existence of ${destPath} in ${orgName}/${destRepo}`) - const resp = await githubDest.repos.getContent({ - owner: orgName, - repo: destRepo, - path: destPath, - ref: 'main' - }) - if (!Array.isArray(resp.data)) { - robot.log.info(`Found ${destPath} in ${orgName}/${destRepo}`) - exists = true - existingContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') - } - } catch (err) { - if (err.status === 404) { - robot.log.info(`File ${destPath} not found in ${orgName}/${destRepo} (this is fine for both merge strategies)`) - exists = false - existingContent = null - } else { - robot.log.info(`Error checking ${destPath} in ${orgName}/${destRepo}: ${err.message}`) - throw err - } + } catch (err) { + if (err.status === 404) { + robot.log.info(`File ${destPath} not found in ${orgName}/${destRepo} (this is fine for both merge strategies)`); + exists = false; + existingSha = undefined; + } else { + robot.log.error(`Error checking ${destPath} in ${orgName}/${destRepo}: ${err.message}`); + throw err; } - // Merge strategy logic - if (mergeStrategy === 'merge' && exists) { - robot.log.info(`Skipping sync of ${fileName} to ${orgName} (already exists & mergeStrategy=${mergeStrategy})`) - continue + } + if (mergeStrategy === 'merge' && exists) { + robot.log.info(`Skipping sync of ${fileName} to ${orgName} (already exists & mergeStrategy=${mergeStrategy})`); + continue; + } + robot.log.info(`Syncing ${fileName} to ${orgName} (mergeStrategy=${mergeStrategy})`); + try { + let srcContentResp; + const pr = context.payload && context.payload.pull_request; + const srcRef = pr && pr.head && pr.head.sha ? pr.head.sha : 'main'; + srcContentResp = await context.octokit.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fileObj.filename, + ref: srcRef + }); + const data = srcContentResp.data; + if (Array.isArray(data)) { + robot.log.debug(`Skipping directory ${fileObj.filename}`); + continue; } - // For overwrite or merge with no existing file, sync - robot.log.info(`Syncing ${fileName} to ${orgName} (mergeStrategy=${mergeStrategy})`) - // Actual sync logic: create or update file in org repo - try { - // Get source file content from hub repo (use PR head SHA if available, else main) - let srcContentResp - const pr = context.payload && context.payload.pull_request - const srcRef = pr && pr.head && pr.head.sha ? pr.head.sha : 'main' - srcContentResp = await context.octokit.repos.getContent({ - owner: env.SAFE_SETTINGS_HUB_ORG, - repo: env.SAFE_SETTINGS_HUB_REPO, - path: fileObj.filename, - ref: srcRef - }) - const data = srcContentResp.data - if (Array.isArray(data)) { - robot.log.info(`Skipping directory ${fileObj.filename}`) - continue - } - const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') - const encoded = Buffer.from(fileContent, 'utf8').toString('base64') - - // Prepare commit message and branch - const destBaseBranch = 'main' - const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') - const timestamp = Date.now() - const branchName = directPush ? destBaseBranch : `safe-settings-globals-sync/${orgName}-${fileName}-${timestamp}` - - // Create branch if not direct push - if (!directPush) { - try { - const baseRef = await githubDest.rest.git.getRef({ owner: orgName, repo: destRepo, ref: `heads/${destBaseBranch}` }) - const baseSha = baseRef.data.object.sha - await githubDest.rest.git.createRef({ owner: orgName, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) - robot.log.info(`Created branch ${branchName} in ${orgName}/${destRepo}`) - } catch (err) { - if (err.status === 422) { - robot.log.warn(`Branch ${branchName} already exists, continuing`) - } else { - throw err - } - } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8'); + const encoded = Buffer.from(fileContent, 'utf8').toString('base64'); + const destBaseBranch = 'main'; + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1'); + const timestamp = Date.now(); + const branchName = directPush ? destBaseBranch : `safe-settings-globals-sync/${orgName}-${fileName}-${timestamp}`; + await createBranchIfNeeded(githubDest, orgName, destRepo, destBaseBranch, branchName, directPush, robot.log); + await createOrUpdateFile(githubDest, { + owner: orgName, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync globals file '${fileName}' from hub` : `Sync globals file '${fileName}' from hub`, + content: encoded, + branch: branchName, + sha: exists ? existingSha : undefined, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }, robot.log); + if (!directPush) { + try { + const prTitle = `Sync globals file '${fileName}' from hub`; + const prBody = `Automated sync of globals file '${fileName}' from hub to ${orgName}.`; + const created = await githubDest.rest.pulls.create({ owner: orgName, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }); + robot.log.info(`Created PR ${created.data.html_url} in ${orgName}/${destRepo}`) + } catch (prErr) { + robot.log.error(`Failed to create PR in ${orgName}/${destRepo}: ${prErr.message}`) + throw prErr } - - // Create or update file - await githubDest.rest.repos.createOrUpdateFileContents({ - owner: orgName, - repo: destRepo, - path: destPath, - message: directPush ? `Direct sync globals file '${fileName}' from hub` : `Sync globals file '${fileName}' from hub`, - content: encoded, - branch: branchName, - sha: exists ? (await githubDest.repos.getContent({ owner: orgName, repo: destRepo, path: destPath, ref: branchName })).data.sha : undefined, - committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, - author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } - }) - robot.log.info(`Committed ${destPath} to ${orgName}/${destRepo}@${branchName}`) - - // Create PR if not direct push - if (!directPush) { - try { - const prTitle = `Sync globals file '${fileName}' from hub` - const prBody = `Automated sync of globals file '${fileName}' from hub to ${orgName}.` - const created = await githubDest.rest.pulls.create({ owner: orgName, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }) - robot.log.info(`Created PR ${created.data.html_url} in ${orgName}/${destRepo}`) - } catch (prErr) { - robot.log.error(`Failed to create PR in ${orgName}/${destRepo}: ${prErr.message}`) - throw prErr - } - } else { - robot.log.info(`Changes pushed directly to ${orgName}/${destRepo}@${destBaseBranch}`) - } - } catch (syncErr) { - robot.log.error(`Failed to sync globals file ${fileName} to ${orgName}: ${syncErr.message}`) + } else { + robot.log.info(`Changes pushed directly to ${orgName}/${destRepo}@${destBaseBranch}`) } + } catch (syncErr) { + robot.log.error(`Failed to sync globals file ${fileName} to ${orgName}: ${syncErr.message}`) } } } @@ -497,7 +492,7 @@ async function syncHubGlobalsUpdate (robot, context, files) { * @returns {Promise>} Results of the operation for each organization */ async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { - const path = require('path') + attachFileLogger(robot) const results = [] try { if (!Array.isArray(orgNames) || orgNames.length === 0) return results @@ -585,6 +580,7 @@ async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { results.push({ org: orgName, error: `failed to check destination: ${probeErr.message}` }) continue } + // 404 -> not present, proceed } } catch (e) { diff --git a/lib/routes.js b/lib/routes.js index 842350601..7a65ac847 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -605,6 +605,54 @@ function setupRoutes (robot, getRouter) { } }) + + // GET /api/safe-settings/hub/log + // Returns parsed log entries (JSON): [{ timestamp, level, message }, ...] + router.get('/api/safe-settings/hub/log', async (req, res) => { + const lines = parseInt(req.query.lines || process.env.SAFE_SETTINGS_LOG_FILE_MAX_LINES || '1000', 10) + const levelsQuery = req.query.levels // comma-separated e.g. 'ERROR,WARN' + const allowedLevels = levelsQuery ? new Set(String(levelsQuery).split(',').map(s => s.trim().toUpperCase()).filter(Boolean)) : null + + const candidates = [] + if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE) + candidates.push(path.join(rootDir, 'safe-settings.log')) + candidates.push(path.join(rootDir, '..', 'safe-settings.log')) + candidates.push(path.join(rootDir, 'ui', 'safe-settings.log')) + + let found = null + for (const p of candidates) { + if (!p) continue + try { + const st = await fs.promises.stat(p) + if (st && st.isFile()) { found = p; break } + } catch (e) { + // ignore + } + } + if (!found) return res.status(404).json({ error: 'Log file not found' }) + + try { + const data = await fs.promises.readFile(found, 'utf8') + const arr = data.split(/\r?\n/).filter(Boolean) + const tail = arr.slice(-lines) + const parsed = tail.map(line => { + // Expecting format: 2025-09-10T12:34:56.789Z [INFO] message + const m = line.match(/^(\d{4}-\d{2}-\d{2}T[^\s]+)\s+\[([A-Z]+)\]\s+(.*)$/) + if (m) { + return { timestamp: m[1], level: m[2], message: m[3], raw: line } + } + // fallback: try to extract level in brackets + const m2 = line.match(/\[([A-Z]+)\]\s*(.*)$/) + if (m2) return { timestamp: null, level: m2[1], message: m2[2], raw: line } + return { timestamp: null, level: 'UNKNOWN', message: line, raw: line } + }) + const filtered = allowedLevels ? parsed.filter(p => allowedLevels.has(String(p.level).toUpperCase())) : parsed + return res.json({ count: filtered.length, entries: filtered }) + } catch (err) { + return res.status(500).json({ error: err && err.message ? err.message : String(err) }) + } + }) + return router } diff --git a/package-lock.json b/package-lock.json index 2d0164b43..6fb37db30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "eta": "^3.5.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", - "minimatch": "^10.0.1", + "minimatch": "^10.0.3", "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", @@ -1324,6 +1324,27 @@ "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "license": "MIT" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -5046,6 +5067,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/before-after-hook": { @@ -5112,15 +5134,6 @@ "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -10864,12 +10877,12 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" diff --git a/package.json b/package.json index 63a2337da..8c6aee56d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "eta": "^3.5.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", - "minimatch": "^10.0.1", + "minimatch": "^10.0.3", "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", diff --git a/safe-settings.log b/safe-settings.log new file mode 100644 index 000000000..962c13d3d --- /dev/null +++ b/safe-settings.log @@ -0,0 +1,54 @@ +2025-09-11T01:43:41.125Z [INFO] Received 'pull_request.closed' event: 45 +2025-09-11T01:43:41.125Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-11T01:43:42.072Z [INFO] Files changed in PR #45: .github/safe-settings/globals/suborg.yml +2025-09-11T01:43:42.072Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-11T01:43:42.073Z [INFO] Syncing safe settings for 'globals/'. +2025-09-11T01:43:42.359Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-11T01:43:42.361Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml +2025-09-11T01:43:42.361Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-11T01:43:42.361Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-11T01:43:42.988Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T01:43:43.292Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) +2025-09-11T01:43:43.292Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T01:43:43.292Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-11T01:43:43.773Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T01:43:44.077Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T01:43:44.078Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) +2025-09-11T01:43:44.078Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-11T01:43:44.793Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T01:43:45.082Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T01:43:45.082Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) +2025-09-11T01:43:45.082Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-11T01:43:45.593Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-11T01:43:45.593Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-11T01:43:46.208Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T01:43:46.461Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) +2025-09-11T01:43:46.461Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T01:43:46.461Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-11T01:43:46.897Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-11T01:43:46.897Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-11T01:43:47.342Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/api/logs/route.js b/ui/src/app/api/logs/route.js new file mode 100644 index 000000000..ec873364a --- /dev/null +++ b/ui/src/app/api/logs/route.js @@ -0,0 +1,28 @@ +import fs from 'fs/promises' +import path from 'path' + +export const dynamic = 'force-static' + +async function findLogFile () { + const candidates = [] + if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE) + candidates.push(path.join(process.cwd(), 'safe-settings.log')) + candidates.push(path.join(process.cwd(), '..', 'safe-settings.log')) + candidates.push(path.join(process.cwd(), '..', '..', 'safe-settings.log')) + + for (const p of candidates) { + if (!p) continue + try { + const st = await fs.stat(p) + if (st && st.isFile()) return p + } catch (e) { + // ignore + } + } + return null +} + +export async function GET () { + const msg = 'Disabled in static export: use the backend endpoint /api/safe-settings/logs or set SAFE_SETTINGS_LOG_FILE to point at the log file.' + return new Response(msg, { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' } }) +} diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx index f24b10862..893537339 100644 --- a/ui/src/app/components/TitleBar.jsx +++ b/ui/src/app/components/TitleBar.jsx @@ -108,21 +108,39 @@ export default function TitleBar() { )} +
  • + + + + + About + {pathname === "/dashboard/help" && ( + + )} + +
  • +
  • + + + + + Sync-Logs + {pathname === "/dashboard/logs" && ( + + )} + +
  • - - - - - About - {pathname === "/dashboard/help" && ( - - )} - diff --git a/ui/src/app/dashboard/logs/page.jsx b/ui/src/app/dashboard/logs/page.jsx new file mode 100644 index 000000000..184e6da63 --- /dev/null +++ b/ui/src/app/dashboard/logs/page.jsx @@ -0,0 +1,110 @@ +"use client" +import TitleBar from '../../components/TitleBar' +import { useState } from 'react' + +export default function LogsPage () { + // Static mock data for demonstration + const mockEntries = [ + { timestamp: '2025-09-11T10:00:00.000Z', level: 'INFO', message: 'Safe Settings service started.' }, + { timestamp: '2025-09-11T10:01:05.123Z', level: 'WARN', message: 'Config file missing, using defaults.' }, + { timestamp: '2025-09-11T10:02:10.456Z', level: 'ERROR', message: 'Failed to sync settings: network error.' }, + { timestamp: '2025-09-11T10:03:00.789Z', level: 'DEBUG', message: 'Polling GitHub API for updates.' }, + { timestamp: '2025-09-11T10:04:15.000Z', level: 'INFO', message: 'Sync completed successfully.' }, + { timestamp: '2025-09-11T10:05:00.000Z', level: 'INFO', message: 'SYNC: Organization settings updated.' }, + { timestamp: '2025-09-11T10:06:00.000Z', level: 'ERROR', message: 'SYNC: Failed to update organization settings.' } + ] + + const logLevels = ['INFO', 'WARN', 'DEBUG', 'ERROR'] + const [selectedLevels, setSelectedLevels] = useState(new Set(logLevels)) + const [search, setSearch] = useState('') + + const toggleLevel = (lvl) => { + const next = new Set(selectedLevels) + if (next.has(lvl)) next.delete(lvl) + else next.add(lvl) + setSelectedLevels(next) + } + + const filtered = mockEntries.filter(e => + selectedLevels.has(e.level.toUpperCase()) && + (search.trim() === '' || e.message.toLowerCase().includes(search.trim().toLowerCase())) + ) + + return ( + <> + +
    +
    +
    +
    +

    Safe Settings Log

    +

    View recent log entries for Safe Settings operations and syncs.

    +
    +
    +
    +
    +
    +
    +
    Filter Options
    +
    + Log Levels: +
    + {logLevels.map(lvl => ( + + ))} +
    +
    +
    + Search Message: + setSearch(e.target.value)} + style={{ maxWidth: 300 }} + /> +
    +
    +
    +
    +
    +
    +
    +
    Log Entries
    +
    + + + + + + + + + + {filtered.map((row, i) => { + let levelClass = '' + if (row.level === 'ERROR') levelClass = 'log-error' + else if (row.level === 'WARN') levelClass = 'log-warn' + return ( + + + + + + ) + })} + +
    TimestampLevelMessage
    {row.timestamp || '-'}{row.level || 'UNKNOWN'}{row.message}
    + {filtered.length === 0 &&
    No log entries match your filters.
    } +
    +
    +
    +
    +
    + + ) +} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index 09a592e7c..0dbf38c60 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -266,4 +266,12 @@ tr td { background-color: var(--bg-primary) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; +} + +.log-error { + color: #c00 !important; +} + +.log-warn { + color: #b8860b !important; } \ No newline at end of file From 5188801a72e0c979942438a9c3f3914d8b828888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Thu, 2 Oct 2025 11:01:32 -0400 Subject: [PATCH 5/5] updated README --- docs/hubSyncHandler/README.md | 198 ++++++++++++++++++++++++++++++++-- 1 file changed, 190 insertions(+), 8 deletions(-) diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index df08f0f6f..480acd2b3 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -4,25 +4,208 @@ ## Overview -This feature adds a hub‑and‑spoke synchronization capability to Safe Settings. +This adds the **hub‑and‑spoke synchronization capability** to Safe Settings. -One central **master admin repository** (the hub) serves as the authoritative source of configuration which is automatically propagated to each organization’s **admin repository** (the spokes). +One central **master admin repository** (the hub) serves as the authoritative source of configuration ('Master' Admin Config Repo) which is automatically propagated to each organization’s **admin repository** (the spokes). -**Note:** When something changes in the central repo, only those changed files are copied to each affected ORG’s admin repo, so everything stays in sync with little manual work. +**Note:** When something changes in the 'Master' repo (the hub), only those changed files are copied to each affected ORG’s admin repo, so everything stays in sync. ## Sync Lifecycle (High Level) ```mermaid graph TD -A0(PR Closed) --> A1(HUB Admin Repo) +A0(PR Closed Event) --> A1(HUB Admin Repo) A1(ORG Admin Repo) --> B(ORG Admin Repo) A1(HUB Admin Repo) --> C(ORG Admin Repo) A1(HUB Admin Repo) --> D(ORG Admin Repo) ``` -## Environment Variables & Inputs +## Gettings Started -Environment variables specific to the 'Sync-Feature' +>**Note:** for the standard setup lets assume that Safe-Settings configuration on the Admin Config Repos (Spokes) are stored in `.github/` + +These are the basic steps to setup the Enterprise-Level Safe-Settings, using **Hub-sync** support. + +### ✅ Step 1: Register the App +**Register the Safe-Settings App** in your Enterprise (Enterprise App) or in your Organization. + +For App "installation tragets" (Where can this GitHub App be installed?) +Choose ***Any account*** + +### ✅ Step 2: Install the App +**Install the Safe-Settings App** in any Organzation that you would like Safe-Settings to manage. + +### ✅ Step 3: Create the 'Org-Level' Safe-Settings Admin Config Repo (Spokes) +Create the Org-Level Repo that is your dedicated Safe-Settings Config Repo and will hold all Safe-Settings configurations for the Org. + +### ✅ Step 4: Create the 'Master' Safe-Settings Admin Config Repo (Hub) +Choose any Organization where the Safe-Settings App is installed and create a 'Master' Safe-Settings Admin Config Repo. + +The Repository requires a standard directory structure for storing the config data: +```bash +.github/ +└─ safe-settings/ + ├── globals/ + │ └── manifest.yml + └── organizations/ + ├── org1/ + │ └── ...yml + └── org2/ + └── ...yml +``` + +Notes: +- The `manifest.yml` is a required file, that defines rules for syncing **Global** Safe-Settings configurations. We will address the content format later. +- `org1` and `org2` are just examples and should be replaced with the real names of the Orgs that you want to manage with the **Hub-Sync**. + +### ✅ Step 5: Configure the 'Master' Safe-Settings Admin Config Repo (Hub) + +The **Hub-Sync** feature supports two options +1. **Organization Sync:** +Any settings file in the `organizations/` directory will be synced to the specific `` (Spoke) Admin config Repo subfolder (eg.: /.github/). Only updated files are sync'd to the ORG admin config Repo (spokes). +1. **Global Sync:** Any settings file in the `globals/` directory will be synced to the specific `` (Spoke) Admin config Repo subfolder (eg.: /.github/). + + :warning: The actual sync operation is based on the rules defined in the `globals/manifest.yml`. The rules provide fine grained control over the sync targets and sync strategy. + +These two options only require that you provide the files you would like to sync, in the correct sub-directory. + +#### ✅ Step 5.1: Configure the `manifest.yml` in the 'Master' Safe-Settings Admin Config Repo (Hub) + +The `manifest.yml` defines the sync rules for global settings distribution. +- Sample `manifest.yml` + + ``` + rules: + - name: global-defaults + # specify the target ORG(s) + targets: + - "*" + files: + - "*.yml" + + # mergeStrategy: merge | overwrite | preserve + # -------------------------------------------- + # merge = use a PR to sync files + # overwrite = sync all files to the target ORG(s) (no PR) + mergeStrategy: merge + + - name: security-policies + # specify the target ORG(s) + targets: + - "acme-*" + - "foo-bar" + files: + - settings.yml + mergeStrategy: overwrite + + # optional toggle, default true + # enabled: false + ``` + +### Example Rule Breakdown + +```yaml +- name: global-defaults + targets: + - "*" + files: + - "*.yml" + mergeStrategy: merge +``` +- **Purpose:** Sync all YAML files to all organizations, merging changes via PR. + +```yaml +- name: security-policies + targets: + - "acme-*" + - "foo-bar" + files: + - settings.yml + mergeStrategy: overwrite + enabled: false +``` + +- **Purpose:** Overwrite `settings.yml` in specific organizations, but currently disabled. + + +### `manifest.yml` Reference + +The `manifest.yml` file defines synchronization rules for Safe-Settings hub-and-spoke configuration management. Each rule specifies which organizations and files to target, and how to handle synchronization. + +### Top-Level Structure + +```yaml +rules: + - name: + targets: [, ...] + files: [, ...] + mergeStrategy: + enabled: # optional + # ...additional fields as needed +``` + +--- + +### Elements + +#### `rules` +- **Type:** Array of objects +- **Description:** List of synchronization rules. Each rule controls how specific files are synced to target organizations. + +#### Rule Object + +##### `name` +- **Type:** String +- **Description:** Unique identifier for the rule. Used for reference and logging. +- **Example:** `global-defaults`, `security-policies` + +##### `targets` +- **Type:** Array of strings +- **Description:** List of organization names or patterns to apply the rule to. + - `"*"`: All organizations + - `"acme-*"`: Organizations with names starting with `acme-` + - `"foo-bar"`: Specific organization +- **Example:** + ```yaml + targets: + - "*" + - "acme-*" + - "foo-bar" + ``` + +##### `files` +- **Type:** Array of strings +- **Description:** File patterns to sync. Supports wildcards. + - `"*.yml"`: All YAML files + - `"settings.yml"`: Specific file +- **Example:** + ```yaml + files: + - "*.yml" + - "settings.yml" + ``` + +##### `mergeStrategy` +- **Type:** String (`merge`, `overwrite`, `preserve`) +- **Description:** Determines how files are synced: + - `merge`: use a PR to sync files + - `overwrite`: Sync all files, replacing existing ones (direct commit, no PR) +- **Example:** + ```yaml + mergeStrategy: merge + ``` + +##### `enabled` +- **Type:** Boolean (optional) +- **Description:** Toggle to enable or disable the rule. Default is `true`. +- **Example:** + ```yaml + enabled: false + ``` + +--- + +### Environment Variables & Inputs Specific to the **Hub-Sync** feature | Name | Purpose | Default | |------|---------|---------| @@ -79,5 +262,4 @@ The following table summarizes the Safe Settings API endpoints: GET /api/safe-settings/env ``` ---- - +--- \ No newline at end of file