diff --git a/.eslintrc.json b/.eslintrc.json
index 09ee628c2..fb129879f 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -42,7 +42,7 @@
"react/prop-types": "off",
"require-jsdoc": "off",
"no-async-promise-executor": "off",
- "@typescript-eslint/no-explicit-any": "warn", // temporary until TS refactor is complete
+ "@typescript-eslint/no-explicit-any": "off", // temporary until TS refactor is complete
"@typescript-eslint/no-unused-vars": "off", // temporary until TS refactor is complete
"@typescript-eslint/no-require-imports": "off", // prevents error on old "require" imports
"@typescript-eslint/no-unused-expressions": "off" // prevents error on test "expect" expressions
diff --git a/.github/ISSUE_TEMPLATE/meeting_minutes.md b/.github/ISSUE_TEMPLATE/meeting_minutes.md
new file mode 100644
index 000000000..ad291fee2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/meeting_minutes.md
@@ -0,0 +1,49 @@
+---
+name: "\U0001F91D GitProxy Meeting Minutes"
+about: To track GitProxy meeting agenda and attendance
+title: DD MMM YYYY - GitProxy Meeting Minutes
+labels: meeting
+assignees:
+
+---
+
+ ## Date
+YYYYMMDD - time
+
+## Meeting info
+- [Meeting link](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595)
+
+- [Register for future meetings](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595&invite=true)
+
+## Untracked attendees
+- Full Name, Affiliation, (optional) GitHub username
+- ...
+
+## Meeting notices
+- FINOS **Project leads** are responsible for observing the FINOS guidelines for [running project meetings](https://community.finos.org/docs/governance/meeting-procedures/). Project maintainers can find additional resources in the [FINOS Maintainers Cheatsheet](https://community.finos.org/docs/finos-maintainers-cheatsheet).
+
+- **All participants** in FINOS project meetings are subject to the [LF Antitrust Policy](https://www.linuxfoundation.org/antitrust-policy/), the [FINOS Community Code of Conduct](https://community.finos.org/docs/governance/code-of-conduct) and all other [FINOS policies](https://community.finos.org/docs/governance/#policies).
+
+- FINOS meetings involve participation by industry competitors, and it is the intention of FINOS and the Linux Foundation to conduct all of its activities in accordance with applicable antitrust and competition laws. It is therefore extremely important that attendees adhere to meeting agendas, and be aware of, and not participate in, any activities that are prohibited under applicable US state, federal or foreign antitrust and competition laws. Please contact legal@finos.org with any questions.
+
+- FINOS project meetings may be recorded for use solely by the FINOS team for administration purposes. In very limited instances, and with explicit approval, recordings may be made more widely available.
+
+## Agenda
+- [ ] Convene & roll call (5mins)
+- [ ] Display [FINOS Antitrust Policy summary slide](https://community.finos.org/Compliance-Slides/Antitrust-Compliance-Slide.pdf)
+- [ ] Review Meeting Notices (see above)
+- [ ] Approve past meeting minutes
+- [ ] Agenda item 1
+- [ ] Agenda item 2
+- [ ] ...
+- [ ] AOB, Q&A & Adjourn (5mins)
+
+## Decisions Made
+- [ ] Decision 1
+- [ ] Decision 2
+- [ ] ...
+
+## Action Items
+- [ ] Action 1
+- [ ] Action 2
+- [ ] ...
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d5a9ccadd..19da6f459 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
@@ -33,7 +33,7 @@ jobs:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ matrix.node-version }}
@@ -52,7 +52,7 @@ jobs:
npm run test-coverage-ci --workspaces --if-present
- name: Upload test coverage report
- uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
+ uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with:
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
@@ -60,8 +60,8 @@ jobs:
# if: ${{ steps.test.outputs.exit_code }} != 0
# run: exit ${{ steps.test.outputs.exit_code }}
- - name: Build application
- run: npm run build
+ - name: Build frontend
+ run: npm run build-ui
- name: Save build folder
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
@@ -71,13 +71,13 @@ jobs:
path: build
- name: Download the build folders
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: build
path: build
- name: Run cypress test
- uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
+ uses: cypress-io/github-action@be1bab96b388bbd9ce3887e397d373c8557e15af # v6.9.2
with:
start: npm start &
wait-on: "http://localhost:3000"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 8f6cd1eb7..51577cfcf 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -51,7 +51,7 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2
with:
egress-policy: audit
@@ -60,7 +60,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
+ uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -74,7 +74,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
+ uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3
# âšī¸ Command-line programs to run using the OS shell.
# đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -87,6 +87,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
+ uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index fdbc7ecb1..6508003ab 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -10,14 +10,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Dependency Review
- uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
+ uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4
with:
comment-summary-in-pr: always
fail-on-severity: high
diff --git a/.github/workflows/experimental-inventory-ci.yml b/.github/workflows/experimental-inventory-ci.yml
index bca99dc2f..f8c1b728c 100644
--- a/.github/workflows/experimental-inventory-ci.yml
+++ b/.github/workflows/experimental-inventory-ci.yml
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
@@ -33,7 +33,7 @@ jobs:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ matrix.node-version }}
diff --git a/.github/workflows/experimental-inventory-cli-publish.yml b/.github/workflows/experimental-inventory-cli-publish.yml
index 3387e285c..8fbf7c3e2 100644
--- a/.github/workflows/experimental-inventory-cli-publish.yml
+++ b/.github/workflows/experimental-inventory-cli-publish.yml
@@ -14,14 +14,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# Setup .npmrc file to publish to npm
- - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
diff --git a/.github/workflows/experimental-inventory-publish.yml b/.github/workflows/experimental-inventory-publish.yml
index 7f16f9bca..fdeac8cf3 100644
--- a/.github/workflows/experimental-inventory-publish.yml
+++ b/.github/workflows/experimental-inventory-publish.yml
@@ -14,14 +14,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# Setup .npmrc file to publish to npm
- - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 8739eff7c..0da265d7e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps: # list of steps
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2
with:
egress-policy: audit
- name: Install NodeJS
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}
diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml
index 2815640c2..274019886 100644
--- a/.github/workflows/npm.yml
+++ b/.github/workflows/npm.yml
@@ -10,18 +10,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# Setup .npmrc file to publish to npm
- - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
- node-version: '18.x'
+ node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
+ env:
+ IS_PUBLISHING: 'YES'
- run: npm publish --access=public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml
index c67da0be0..6d18d2d98 100644
--- a/.github/workflows/pr-lint.yml
+++ b/.github/workflows/pr-lint.yml
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
diff --git a/.github/workflows/sample-publish.yml b/.github/workflows/sample-publish.yml
index 005af5ce1..b6db78b02 100644
--- a/.github/workflows/sample-publish.yml
+++ b/.github/workflows/sample-publish.yml
@@ -13,15 +13,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# Setup .npmrc file to publish to npm
- - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
- node-version: '18.x'
+ node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: publish sample package
run: npm install --include peer && npm publish --access=public
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index ecfc13e42..20d892b7c 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
@@ -72,6 +72,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
+ uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
sarif_file: results.sarif
diff --git a/.github/workflows/unused-dependencies.yml b/.github/workflows/unused-dependencies.yml
index 39071e270..b0230dfa7 100644
--- a/.github/workflows/unused-dependencies.yml
+++ b/.github/workflows/unused-dependencies.yml
@@ -9,19 +9,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2
+ uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: 'Setup Node.js'
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
- node-version: '18.x'
+ node-version: '20.x'
- name: 'Run depcheck'
run: |
- npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,mocha,ts-mocha,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths"
+ npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,mocha,ts-mocha,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths,jsonschema"
echo $?
if [[ $? == 1 ]]; then
echo "Unused dependencies or devDependencies found"
diff --git a/.gitignore b/.gitignore
index 1849589c4..747f84c76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -263,4 +263,10 @@ yarn-error.log*
# Docusaurus website
website/build
-website/.docusaurus
\ No newline at end of file
+website/.docusaurus
+
+# git-config-cache
+.git-config-cache
+
+# Jetbrains IDE
+.idea
diff --git a/.npmignore b/.npmignore
index 27087e67d..286c7a75f 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,3 +1,5 @@
# This file required to override .gitignore when publishing to npm
website/
plugins/
+experimental/
+cypress/
diff --git a/README.md b/README.md
index c6591558e..98740e769 100644
--- a/README.md
+++ b/README.md
@@ -27,14 +27,13 @@
[](https://www.npmjs.com/package/@finos/git-proxy)
[](https://github.com/finos/git-proxy/actions/workflows/ci.yml)
[](https://codecov.io/gh/finos/git-proxy)
-[](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy)
[](https://git-proxy.finos.org)
[](https://github.com/finos/git-proxy/blob/main/LICENSE)
[](https://github.com/finos/git-proxy/graphs/contributors)
[](https://app.slack.com/client/T01E7QRQH97/C06LXNW0W76)
-[](https://github.com/finos/git-proxy/stargazers)
-[](https://github.com/finos/git-proxy/forks)
+[](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy)
+[](https://www.bestpractices.dev/projects/10520)
@@ -85,6 +84,7 @@ $ git push proxy $(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remo
Using the default configuration, GitProxy intercepts the push and _blocks_ it. To enable code pushing to your fork via GitProxy, add your repository URL into the GitProxy config file (`proxy.config.json`). For more information, refer to [our documentation](https://git-proxy.finos.org).
## Documentation
+
For detailed step-by-step instructions for how to install, deploy & configure GitProxy and
customize for your environment, see the [project's documentation](https://git-proxy.finos.org/docs/):
@@ -102,11 +102,11 @@ If you identify a security vulnerability in the codebase, please follow the step
## Code of Conduct
-We are committed to making open source an enjoyable and respectful experience for our community. See CODE_OF_CONDUCT
for more information.
+We are committed to making open source an enjoyable and respectful experience for our community. See [`CODE_OF_CONDUCT`](CODE_OF_CONDUCT.md) for more information.
## License
-This project is distributed under the Apache-2.0 license. See LICENSE
for more information.
+This project is distributed under the Apache-2.0 license. See [`LICENSE`](LICENSE) for more information.
## Contact
@@ -116,4 +116,4 @@ If you can't access Slack, you can also [subscribe to our mailing list](mailto:g
Join our [fortnightly Zoom meeting](https://zoom.us/j/97235277537?pwd=aDJsaE8zcDJpYW1vZHJmSTJ0RXNZUT09) on Monday, 11AM EST (odd week numbers). Send an e-mail to [help@finos.org](mailto:help@finos.org) to get a calendar invitation.
-Otherwise, if you have a deeper query or require more support, please [raise an issue](https://github.com/finos/git-proxy/issues).
+Otherwise, if you have a deeper query or require more support, please [raise an issue](https://github.com/finos/git-proxy/issues).
diff --git a/config.schema.json b/config.schema.json
index 4e9622ca0..78cc005c8 100644
--- a/config.schema.json
+++ b/config.schema.json
@@ -24,6 +24,30 @@
"description": "Provide domains to use alternative to the defaults",
"type": "object"
},
+ "rateLimit": {
+ "description": "API Rate limiting configuration.",
+ "type": "object",
+ "properties": {
+ "windowMs": {
+ "type": "number",
+ "description": "How long to remember requests for, in milliseconds (default 10 mins)."
+ },
+ "limit": {
+ "type": "number",
+ "description": "How many requests to allow (default 150)."
+ },
+ "statusCode": {
+ "type": "number",
+ "description": "HTTP status code after limit is reached (default is 429)."
+ },
+ "message": {
+ "type": "string",
+ "description": "Response to return after limit is reached."
+ }
+ },
+ "required": ["windowMs", "limit"],
+ "additionalProperties": false
+ },
"privateOrganizations": {
"description": "Pattern searches for listed private organizations are disabled",
"type": "array"
@@ -88,6 +112,18 @@
"cert": { "type": "string" }
},
"required": ["enabled", "key", "cert"]
+ },
+ "configurationSources": {
+ "enabled": { "type": "boolean" },
+ "reloadIntervalSeconds": { "type": "number" },
+ "merge": { "type": "boolean" },
+ "sources": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Configuration source"
+ }
+ }
}
},
"definitions": {
diff --git a/experimental/li-cli/package-lock.json b/experimental/li-cli/package-lock.json
index 9b3276498..5636e7646 100644
--- a/experimental/li-cli/package-lock.json
+++ b/experimental/li-cli/package-lock.json
@@ -9,22 +9,22 @@
"version": "0.0.1",
"license": "Apache-2.0",
"dependencies": {
- "@inquirer/prompts": "^7.3.3",
- "yaml": "^2.7.0",
+ "@inquirer/prompts": "^7.5.0",
+ "yaml": "^2.7.1",
"yargs": "^17.7.2",
- "zod": "^3.24.2"
+ "zod": "^3.24.4"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
- "@types/node": "^22.13.10",
+ "@types/node": "^22.15.12",
"@types/yargs": "^17.0.33",
"jest": "^29.7.0",
"rimraf": "^6.0.1",
- "ts-jest": "^29.2.6",
+ "ts-jest": "^29.3.2",
"ts-node": "^10.9.2",
- "tsc-alias": "^1.8.11",
+ "tsc-alias": "^1.8.16",
"tslib": "^2.8.1",
- "typescript": "^5.8.2"
+ "typescript": "^5.8.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -563,14 +563,14 @@
}
},
"node_modules/@inquirer/checkbox": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.3.tgz",
- "integrity": "sha512-KU1MGwf24iABJjGESxhyj+/rlQYSRoCfcuHDEHXfZ1DENmbuSRfyrUb+LLjHoee5TNOFKwaFxDXc5/zRwJUPMQ==",
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz",
+ "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
+ "@inquirer/core": "^10.1.10",
"@inquirer/figures": "^1.0.11",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2",
"yoctocolors-cjs": "^2.1.2"
},
@@ -587,13 +587,13 @@
}
},
"node_modules/@inquirer/confirm": {
- "version": "5.1.7",
- "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.7.tgz",
- "integrity": "sha512-Xrfbrw9eSiHb+GsesO8TQIeHSMTP0xyvTCeeYevgZ4sKW+iz9w/47bgfG9b0niQm+xaLY2EWPBINUPldLwvYiw==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz",
+ "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
- "@inquirer/type": "^3.0.5"
+ "@inquirer/core": "^10.1.10",
+ "@inquirer/type": "^3.0.6"
},
"engines": {
"node": ">=18"
@@ -608,13 +608,13 @@
}
},
"node_modules/@inquirer/core": {
- "version": "10.1.8",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.8.tgz",
- "integrity": "sha512-HpAqR8y715zPpM9e/9Q+N88bnGwqqL8ePgZ0SMv/s3673JLMv3bIkoivGmjPqXlEgisUksSXibweQccUwEx4qQ==",
+ "version": "10.1.10",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz",
+ "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==",
"license": "MIT",
"dependencies": {
"@inquirer/figures": "^1.0.11",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
@@ -649,13 +649,13 @@
}
},
"node_modules/@inquirer/editor": {
- "version": "4.2.8",
- "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.8.tgz",
- "integrity": "sha512-UkGKbMFlQw5k4ZLjDwEi5z8NIVlP/3DAlLHta0o0pSsdpPThNmPtUL8mvGCHUaQtR+QrxR9yRYNWgKMsHkfIUA==",
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz",
+ "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/core": "^10.1.10",
+ "@inquirer/type": "^3.0.6",
"external-editor": "^3.1.0"
},
"engines": {
@@ -671,13 +671,13 @@
}
},
"node_modules/@inquirer/expand": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.10.tgz",
- "integrity": "sha512-leyBouGJ77ggv51Jb/OJmLGGnU2HYc13MZ2iiPNLwe2VgFgZPVqsrRWSa1RAHKyazjOyvSNKLD1B2K7A/iWi1g==",
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz",
+ "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/core": "^10.1.10",
+ "@inquirer/type": "^3.0.6",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
@@ -702,13 +702,13 @@
}
},
"node_modules/@inquirer/input": {
- "version": "4.1.7",
- "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.7.tgz",
- "integrity": "sha512-rCQAipJNA14UTH84df/z4jDJ9LZ54H6zzuCAi7WZ0qVqx3CSqLjfXAMd5cpISIxbiHVJCPRB81gZksq6CZsqDg==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz",
+ "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
- "@inquirer/type": "^3.0.5"
+ "@inquirer/core": "^10.1.10",
+ "@inquirer/type": "^3.0.6"
},
"engines": {
"node": ">=18"
@@ -723,13 +723,13 @@
}
},
"node_modules/@inquirer/number": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.10.tgz",
- "integrity": "sha512-GLsdnxzNefjCJUmWyjaAuNklHgDpCTL4RMllAVhVvAzBwRW9g38eZ5tWgzo1lirtSDTpsh593hqXVhxvdrjfwA==",
+ "version": "3.0.12",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz",
+ "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
- "@inquirer/type": "^3.0.5"
+ "@inquirer/core": "^10.1.10",
+ "@inquirer/type": "^3.0.6"
},
"engines": {
"node": ">=18"
@@ -744,13 +744,13 @@
}
},
"node_modules/@inquirer/password": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.10.tgz",
- "integrity": "sha512-JC538ujqeYKkFqLoWZ0ILBteIUO2yajBMVEUZSxjl9x6fiEQtM+I5Rca7M2D8edMDbyHLnXifGH1hJZdh8V5rA==",
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz",
+ "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/core": "^10.1.10",
+ "@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2"
},
"engines": {
@@ -766,21 +766,21 @@
}
},
"node_modules/@inquirer/prompts": {
- "version": "7.3.3",
- "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.3.tgz",
- "integrity": "sha512-QS1AQgJ113iE/nmym03yKZKHvGjVWwkGZT3B1yKrrMG0bJKQg1jUkntFP8aPd2FUQzu/nga7QU2eDpzIP5it0Q==",
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.0.tgz",
+ "integrity": "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==",
"license": "MIT",
"dependencies": {
- "@inquirer/checkbox": "^4.1.3",
- "@inquirer/confirm": "^5.1.7",
- "@inquirer/editor": "^4.2.8",
- "@inquirer/expand": "^4.0.10",
- "@inquirer/input": "^4.1.7",
- "@inquirer/number": "^3.0.10",
- "@inquirer/password": "^4.0.10",
- "@inquirer/rawlist": "^4.0.10",
- "@inquirer/search": "^3.0.10",
- "@inquirer/select": "^4.0.10"
+ "@inquirer/checkbox": "^4.1.5",
+ "@inquirer/confirm": "^5.1.9",
+ "@inquirer/editor": "^4.2.10",
+ "@inquirer/expand": "^4.0.12",
+ "@inquirer/input": "^4.1.9",
+ "@inquirer/number": "^3.0.12",
+ "@inquirer/password": "^4.0.12",
+ "@inquirer/rawlist": "^4.1.0",
+ "@inquirer/search": "^3.0.12",
+ "@inquirer/select": "^4.2.0"
},
"engines": {
"node": ">=18"
@@ -795,13 +795,13 @@
}
},
"node_modules/@inquirer/rawlist": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.10.tgz",
- "integrity": "sha512-vOQbQkmhaCsF2bUmjoyRSZJBz77UnIF/F3ZS2LMgwbgyaG2WgwKHh0WKNj0APDB72WDbZijhW5nObQbk+TnbcA==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.0.tgz",
+ "integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/core": "^10.1.10",
+ "@inquirer/type": "^3.0.6",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
@@ -817,14 +817,14 @@
}
},
"node_modules/@inquirer/search": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.10.tgz",
- "integrity": "sha512-EAVKAz6P1LajZOdoL+R+XC3HJYSU261fbJzO4fCkJJ7UPFcm+nP+gzC+DDZWsb2WK9PQvKsnaKiNKsY8B6dBWQ==",
+ "version": "3.0.12",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz",
+ "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
+ "@inquirer/core": "^10.1.10",
"@inquirer/figures": "^1.0.11",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/type": "^3.0.6",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
@@ -840,14 +840,14 @@
}
},
"node_modules/@inquirer/select": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.10.tgz",
- "integrity": "sha512-Tg8S9nESnCfISu5tCZSuXpXq0wHuDVimj7xyHstABgR34zcJnLdq/VbjB2mdZvNAMAehYBnNzSjxB06UE8LLAA==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.0.tgz",
+ "integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.8",
+ "@inquirer/core": "^10.1.10",
"@inquirer/figures": "^1.0.11",
- "@inquirer/type": "^3.0.5",
+ "@inquirer/type": "^3.0.6",
"ansi-escapes": "^4.3.2",
"yoctocolors-cjs": "^2.1.2"
},
@@ -864,9 +864,9 @@
}
},
"node_modules/@inquirer/type": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz",
- "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz",
+ "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -1575,13 +1575,13 @@
}
},
"node_modules/@types/node": {
- "version": "22.13.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
- "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
+ "version": "22.15.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz",
+ "integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~6.20.0"
+ "undici-types": "~6.21.0"
}
},
"node_modules/@types/stack-utils": {
@@ -2637,6 +2637,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz",
+ "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -4348,6 +4361,16 @@
"node": ">=8"
}
},
+ "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/resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
@@ -4749,9 +4772,9 @@
}
},
"node_modules/ts-jest": {
- "version": "29.2.6",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz",
- "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==",
+ "version": "29.3.2",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz",
+ "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4763,6 +4786,7 @@
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
"semver": "^7.7.1",
+ "type-fest": "^4.39.1",
"yargs-parser": "^21.1.1"
},
"bin": {
@@ -4810,6 +4834,19 @@
"node": ">=10"
}
},
+ "node_modules/ts-jest/node_modules/type-fest": {
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz",
+ "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -4855,14 +4892,15 @@
}
},
"node_modules/tsc-alias": {
- "version": "1.8.11",
- "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.11.tgz",
- "integrity": "sha512-2DuEQ58A9Rj2NE2c1+/qaGKlshni9MCK95MJzRGhQG0CYLw0bE/ACgbhhTSf/p1svLelwqafOd8stQate2bYbg==",
+ "version": "1.8.16",
+ "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz",
+ "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.3",
"commander": "^9.0.0",
+ "get-tsconfig": "^4.10.0",
"globby": "^11.0.4",
"mylas": "^2.1.9",
"normalize-path": "^3.0.0",
@@ -4870,6 +4908,9 @@
},
"bin": {
"tsc-alias": "dist/bin/index.js"
+ },
+ "engines": {
+ "node": ">=16.20.2"
}
},
"node_modules/tslib": {
@@ -4902,9 +4943,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
- "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4916,9 +4957,9 @@
}
},
"node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT"
},
@@ -5093,9 +5134,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
- "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
+ "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -5167,9 +5208,9 @@
}
},
"node_modules/zod": {
- "version": "3.24.2",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
- "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
+ "version": "3.24.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
+ "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/experimental/li-cli/package.json b/experimental/li-cli/package.json
index 6bb723fb0..794e86446 100644
--- a/experimental/li-cli/package.json
+++ b/experimental/li-cli/package.json
@@ -13,21 +13,21 @@
"test": "jest --forceExit --detectOpenHandles"
},
"dependencies": {
- "@inquirer/prompts": "^7.3.3",
- "yaml": "^2.7.0",
+ "@inquirer/prompts": "^7.5.0",
+ "yaml": "^2.7.1",
"yargs": "^17.7.2",
- "zod": "^3.24.2"
+ "zod": "^3.24.4"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
- "@types/node": "^22.13.10",
+ "@types/node": "^22.15.12",
"@types/yargs": "^17.0.33",
"jest": "^29.7.0",
"rimraf": "^6.0.1",
- "ts-jest": "^29.2.6",
+ "ts-jest": "^29.3.2",
"ts-node": "^10.9.2",
- "tsc-alias": "^1.8.11",
+ "tsc-alias": "^1.8.16",
"tslib": "^2.8.1",
- "typescript": "^5.8.2"
+ "typescript": "^5.8.3"
}
}
diff --git a/index.ts b/index.ts
index 880ccfe02..ffc50b73a 100755
--- a/index.ts
+++ b/index.ts
@@ -2,8 +2,8 @@
/* eslint-disable max-len */
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
-import * as fs from 'fs';
-import { configFile, setConfigFile, validate } from './src/config/file';
+import { existsSync } from 'fs';
+import { configFile, setConfigFile, loadConfig } from './src/config/file';
import proxy from './src/proxy';
import service from './src/service';
@@ -13,37 +13,45 @@ const argv = yargs(hideBin(process.argv))
validate: {
description:
'Check the proxy.config.json file in the current working directory for validation errors.',
- required: false,
alias: 'v',
type: 'boolean',
},
config: {
description: 'Path to custom git-proxy configuration file.',
- default: 'proxy.config.json',
- required: false,
alias: 'c',
type: 'string',
+ default: 'proxy.config.json',
},
})
.strict()
.parseSync();
-setConfigFile(argv.c as string || "");
+setConfigFile(argv.config);
-if (argv.v) {
- if (!fs.existsSync(configFile)) {
+if (argv.validate) {
+ if (!existsSync(configFile)) {
console.error(
- `Config file ${configFile} doesn't exist, nothing to validate! Did you forget -c/--config?`,
+ `â Config file ${configFile} doesn't exist, nothing to validate! Did you forget -c/--config?`,
);
process.exit(1);
}
- validate();
- console.log(`${configFile} is valid`);
- process.exit(0);
+ try {
+ loadConfig();
+ console.log(`âī¸ ${configFile} is valid`);
+ process.exit(0);
+ } catch (err: any) {
+ console.error('â Validation Error:', err.message);
+ process.exit(1);
+ }
}
-validate();
+try {
+ loadConfig();
+} catch (err: any) {
+ console.error('â Validation Error:', err.message);
+ process.exit(1);
+}
proxy.start();
service.start();
diff --git a/package-lock.json b/package-lock.json
index 3052eaddc..2a44462b2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@finos/git-proxy",
- "version": "1.10.0",
+ "version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@finos/git-proxy",
- "version": "1.10.0",
+ "version": "1.14.0",
"license": "Apache-2.0",
"workspaces": [
"./packages/git-proxy-cli"
@@ -25,6 +25,7 @@
"connect-mongo": "^5.1.0",
"cors": "^2.8.5",
"diff2html": "^3.4.33",
+ "env-paths": "^2.2.1",
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"express-rate-limit": "^7.1.5",
@@ -51,7 +52,8 @@
"react-router-dom": "6.28.2",
"simple-git": "^3.25.0",
"uuid": "^11.0.0",
- "yargs": "^17.7.2"
+ "yargs": "^17.7.2",
+ "zod": "^3.24.3"
},
"bin": {
"git-proxy": "index.js",
@@ -92,7 +94,7 @@
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
"typescript": "^5.7.3",
- "vite": "4.5.5",
+ "vite": "^4.5.13",
"vite-tsconfig-paths": "^5.1.4"
},
"optionalDependencies": {
@@ -1034,27 +1036,27 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
- "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+ "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.0"
+ "@babel/template": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
- "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+ "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.26.0"
+ "@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1202,9 +1204,10 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.23.7",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz",
- "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
+ "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
+ "license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -1213,15 +1216,15 @@
}
},
"node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+ "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.26.2",
+ "@babel/parser": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
@@ -1247,9 +1250,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
- "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+ "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4649,9 +4652,9 @@
"dev": true
},
"node_modules/axios": {
- "version": "1.8.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
- "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
+ "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -6167,7 +6170,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -13800,9 +13802,9 @@
}
},
"node_modules/vite": {
- "version": "4.5.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
- "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
+ "version": "4.5.13",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.13.tgz",
+ "integrity": "sha512-Hgp8IF/yZDzKsN1hQWOuQZbrKiaFsbQud+07jJ8h9m9PaHWkpvZ5u55Xw5yYjWRXwRQ4jwFlJvY7T7FUJG9MCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14303,13 +14305,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zod": {
+ "version": "3.24.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
+ "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"packages/git-proxy-cli": {
"name": "@finos/git-proxy-cli",
"version": "0.1.0",
"license": "Apache-2.0",
"dependencies": {
"@finos/git-proxy": "file:../..",
- "axios": "^1.8.4",
+ "axios": "^1.9.0",
"yargs": "^17.7.2"
},
"bin": {
diff --git a/package.json b/package.json
index 757dfbd92..7199f3baa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@finos/git-proxy",
- "version": "1.10.0",
+ "version": "1.14.0",
"description": "Deploy custom push protections and policies on top of Git.",
"scripts": {
"cli": "node ./packages/git-proxy-cli/index.js",
@@ -8,8 +8,11 @@
"clientinstall": "npm install --prefix client",
"server": "tsx index.ts",
"start": "concurrently \"npm run server\" \"npm run client\"",
- "build": "vite build",
- "build-ts": "tsc",
+ "build": "npm run build-ui && npm run build-lib",
+ "build-ui": "vite build",
+ "build-lib": "./scripts/build-for-publish.sh",
+ "restore-lib": "./scripts/undo-build.sh",
+ "check-types": "tsc",
"test": "NODE_ENV=test ts-mocha './test/*.js' --exit",
"test-coverage": "nyc npm run test",
"test-coverage-ci": "nyc --reporter=lcovonly --reporter=text npm run test",
@@ -47,6 +50,7 @@
"connect-mongo": "^5.1.0",
"cors": "^2.8.5",
"diff2html": "^3.4.33",
+ "env-paths": "^2.2.1",
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"express-rate-limit": "^7.1.5",
@@ -73,7 +77,8 @@
"react-router-dom": "6.28.2",
"simple-git": "^3.25.0",
"uuid": "^11.0.0",
- "yargs": "^17.7.2"
+ "yargs": "^17.7.2",
+ "zod": "^3.24.3"
},
"devDependencies": {
"@babel/core": "^7.23.2",
@@ -110,7 +115,7 @@
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
"typescript": "^5.7.3",
- "vite": "4.5.5",
+ "vite": "^4.5.13",
"vite-tsconfig-paths": "^5.1.4"
},
"optionalDependencies": {
diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.js
index b0090a4bf..142a58a33 100755
--- a/packages/git-proxy-cli/index.js
+++ b/packages/git-proxy-cli/index.js
@@ -7,7 +7,8 @@ const util = require('util');
const GIT_PROXY_COOKIE_FILE = 'git-proxy-cookie';
// GitProxy UI HOST and PORT (configurable via environment variable)
-const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 8080 } = process.env;
+const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 8080 } =
+ process.env;
const baseUrl = `${uiHost}:${uiPort}`;
@@ -306,6 +307,29 @@ async function logout() {
console.log('Logout: OK');
}
+/**
+ * Reloads the GitProxy configuration without restarting the process
+ */
+async function reloadConfig() {
+ if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) {
+ console.error('Error: Reload config: Authentication required');
+ process.exitCode = 1;
+ return;
+ }
+
+ try {
+ const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8'));
+
+ await axios.post(`${baseUrl}/api/v1/admin/reload-config`, {}, { headers: { Cookie: cookies } });
+
+ console.log('Configuration reloaded successfully');
+ } catch (error) {
+ const errorMessage = `Error: Reload config: '${error.message}'`;
+ process.exitCode = 2;
+ console.error(errorMessage);
+ }
+}
+
// Parsing command line arguments
yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused-expressions
.command({
@@ -436,6 +460,11 @@ yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused
rejectGitPush(argv.id);
},
})
+ .command({
+ command: 'reload-config',
+ description: 'Reload GitProxy configuration without restarting',
+ action: reloadConfig,
+ })
.demandCommand(1, 'You need at least one command before moving on')
.strict()
.help().argv;
diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json
index baade725c..d8babc8d6 100644
--- a/packages/git-proxy-cli/package.json
+++ b/packages/git-proxy-cli/package.json
@@ -4,7 +4,7 @@
"description": "Command line interface tool for FINOS GitProxy.",
"bin": "./index.js",
"dependencies": {
- "axios": "^1.8.4",
+ "axios": "^1.9.0",
"yargs": "^17.7.2",
"@finos/git-proxy": "file:../.."
},
diff --git a/plugins/git-proxy-plugin-samples/package.json b/plugins/git-proxy-plugin-samples/package.json
index 622b3c3e9..2a9455fcd 100644
--- a/plugins/git-proxy-plugin-samples/package.json
+++ b/plugins/git-proxy-plugin-samples/package.json
@@ -16,6 +16,6 @@
"express": "^4.21.2"
},
"peerDependencies": {
- "@finos/git-proxy": "^1.9.3"
+ "@finos/git-proxy": "^1.11.0"
}
}
diff --git a/proxy.config.json b/proxy.config.json
index fdb32a0d0..2a45cefac 100644
--- a/proxy.config.json
+++ b/proxy.config.json
@@ -2,6 +2,10 @@
"proxyUrl": "https://github.com",
"cookieSecret": "cookie secret",
"sessionMaxAgeHours": 12,
+ "rateLimit": {
+ "windowMs": 60000,
+ "limit": 150
+ },
"tempPassword": {
"sendEmail": false,
"emailConfig": {}
@@ -92,6 +96,39 @@
}
]
},
+ "configurationSources": {
+ "enabled": false,
+ "reloadIntervalSeconds": 60,
+ "merge": false,
+ "sources": [
+ {
+ "type": "file",
+ "enabled": false,
+ "path": "./external-config.json"
+ },
+ {
+ "type": "http",
+ "enabled": false,
+ "url": "http://config-service/git-proxy-config",
+ "headers": {},
+ "auth": {
+ "type": "bearer",
+ "token": ""
+ }
+ },
+ {
+ "type": "git",
+ "enabled": false,
+ "repository": "https://git-server.com/project/git-proxy-config",
+ "branch": "main",
+ "path": "git-proxy/config.json",
+ "auth": {
+ "type": "ssh",
+ "privateKeyPath": "/path/to/.ssh/id_rsa"
+ }
+ }
+ ]
+ },
"domains": {},
"privateOrganizations": [],
"urlShortener": "",
@@ -99,7 +136,7 @@
"csrfProtection": true,
"plugins": [],
"tls": {
- "enabled": true,
+ "enabled": false,
"key": "certs/key.pem",
"cert": "certs/cert.pem"
}
diff --git a/proxy.config.schema.ts b/proxy.config.schema.ts
new file mode 100644
index 000000000..b0dfb46dd
--- /dev/null
+++ b/proxy.config.schema.ts
@@ -0,0 +1,205 @@
+import { z } from 'zod';
+
+const TempPasswordSchema = z.object({
+ sendEmail: z.boolean().default(false),
+ emailConfig: z.record(z.unknown()).default({}),
+});
+
+const AuthorisedItemSchema = z.object({
+ project: z.string(),
+ name: z.string(),
+ url: z.string().regex(/^(?:https?:\/\/.+\.git|git@[^:]+:[^/]+\/.+\.git)$/i, {
+ message: 'Must be a Git HTTPS URL (https://... .git) or SSH URL (git@...:... .git)',
+ }),
+});
+
+const FsSinkSchema = z.object({
+ type: z.literal('fs'),
+ params: z.object({ filepath: z.string() }),
+ enabled: z.boolean().default(true),
+});
+
+const MongoSinkSchema = z.object({
+ type: z.literal('mongo'),
+ connectionString: z.string(),
+ options: z.object({
+ useNewUrlParser: z.boolean().default(true),
+ useUnifiedTopology: z.boolean().default(true),
+ tlsAllowInvalidCertificates: z.boolean().default(false),
+ ssl: z.boolean().default(false),
+ }),
+ enabled: z.boolean().default(false),
+});
+
+const SinkSchema = z.discriminatedUnion('type', [FsSinkSchema, MongoSinkSchema]);
+
+const ActiveDirectoryConfigSchema = z.object({
+ url: z.string(),
+ baseDN: z.string(),
+ searchBase: z.string(),
+});
+
+const LocalAuthSchema = z.object({
+ type: z.literal('local'),
+ enabled: z.boolean().default(true),
+});
+
+const ADAuthSchema = z.object({
+ type: z.literal('ActiveDirectory'),
+ enabled: z.boolean().default(false),
+ adminGroup: z.string().default(''),
+ userGroup: z.string().default(''),
+ domain: z.string().default(''),
+ adConfig: ActiveDirectoryConfigSchema,
+});
+
+const AuthenticationSchema = z.discriminatedUnion('type', [LocalAuthSchema, ADAuthSchema]);
+
+const GithubApiSchema = z.object({
+ baseUrl: z.string().url(),
+});
+
+const CommitEmailSchema = z.object({
+ local: z.object({ block: z.string().default('') }),
+ domain: z.object({ allow: z.string().default('.*') }),
+});
+
+const CommitBlockSchema = z.object({
+ literals: z.array(z.string()).default([]),
+ patterns: z.array(z.string()).default([]),
+});
+
+const CommitDiffSchema = z.object({
+ block: z.object({
+ literals: z.array(z.string()).default([]),
+ patterns: z.array(z.string()).default([]),
+ providers: z.record(z.unknown()).default({}),
+ }),
+});
+
+const AttestationQuestionSchema = z.object({
+ label: z.string(),
+ tooltip: z.object({
+ text: z.string(),
+ links: z.array(z.string()).default([]),
+ }),
+});
+
+export const RateLimitSchema = z
+ .object({
+ windowMs: z.number({ description: 'Sliding window in milliseconds' }),
+ limit: z.number({ description: 'Maximum number of requests' }),
+ statusCode: z.number().optional().default(429),
+ message: z.string().optional().default('Too many requests'),
+ })
+ .strict();
+
+const FileConfigSourceSchema = z
+ .object({
+ type: z.literal('file'),
+ enabled: z.boolean().default(false),
+ path: z.string(),
+ })
+ .strict();
+
+const HttpConfigSourceSchema = z
+ .object({
+ type: z.literal('http'),
+ enabled: z.boolean().default(false),
+ url: z.string().url(),
+ headers: z.record(z.string()).default({}),
+ auth: z
+ .object({
+ type: z.literal('bearer'),
+ token: z.string().default(''),
+ })
+ .strict()
+ .default({ type: 'bearer', token: '' }),
+ })
+ .strict();
+
+const GitConfigSourceSchema = z
+ .object({
+ type: z.literal('git'),
+ enabled: z.boolean().default(false),
+ repository: z.string(),
+ branch: z.string().default('main'),
+ path: z.string(),
+ auth: z
+ .object({
+ type: z.literal('ssh'),
+ privateKeyPath: z.string(),
+ })
+ .strict(),
+ })
+ .strict();
+
+const ConfigSourceSchema = z.discriminatedUnion('type', [
+ FileConfigSourceSchema,
+ HttpConfigSourceSchema,
+ GitConfigSourceSchema,
+]);
+
+export const ConfigurationSourcesSchema = z
+ .object({
+ enabled: z.boolean(),
+ reloadIntervalSeconds: z.number().optional().default(60),
+ merge: z.boolean().optional().default(false),
+ sources: z.array(ConfigSourceSchema).default([]),
+ })
+ .strict();
+
+export const ConfigSchema = z
+ .object({
+ proxyUrl: z.string().url().default('https://github.com'),
+ cookieSecret: z.string().default(''),
+ sessionMaxAgeHours: z.number().int().positive().default(12),
+ rateLimit: RateLimitSchema.default({ windowMs: 600000, limit: 150 }),
+ configurationSources: ConfigurationSourcesSchema.default({
+ enabled: false,
+ reloadIntervalSeconds: 60,
+ merge: false,
+ sources: [],
+ }),
+ tempPassword: TempPasswordSchema.default({}),
+ authorisedList: z.array(AuthorisedItemSchema).default([]),
+ sink: z.array(SinkSchema).default([]),
+ authentication: z.array(AuthenticationSchema).default([{ type: 'local', enabled: true }]),
+ api: z
+ .object({
+ github: GithubApiSchema,
+ })
+ .default({ github: { baseUrl: 'https://api.github.com' } }),
+ commitConfig: z
+ .object({
+ author: z.object({ email: CommitEmailSchema }),
+ message: z.object({ block: CommitBlockSchema }),
+ diff: CommitDiffSchema,
+ })
+ .default({
+ author: { email: { local: { block: '' }, domain: { allow: '.*' } } },
+ message: { block: { literals: [], patterns: [] } },
+ diff: { block: { literals: [], patterns: [], providers: {} } },
+ }),
+ attestationConfig: z
+ .object({
+ questions: z.array(AttestationQuestionSchema).default([]),
+ })
+ .default({ questions: [] }),
+ domains: z.record(z.string(), z.string()).default({}),
+ privateOrganizations: z.array(z.string()).default([]),
+ urlShortener: z.string().default(''),
+ contactEmail: z.string().default(''),
+ csrfProtection: z.boolean().default(true),
+ plugins: z.array(z.unknown()).default([]),
+ tls: z
+ .object({
+ enabled: z.boolean().default(false),
+ key: z.string().default(''),
+ cert: z.string().default(''),
+ })
+ .default({}),
+ })
+ .strict();
+
+export type Config = z.infer;
diff --git a/scripts/build-for-publish.sh b/scripts/build-for-publish.sh
new file mode 100755
index 000000000..1c9ac4130
--- /dev/null
+++ b/scripts/build-for-publish.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# This script allows for emitting js and definitions from the typescript into
+# the same import locations as the original files.
+# When we adjust how we import the library we can move to a "dist" folder and
+# explicit "exports".
+
+if [ "${IS_PUBLISHING:-}" != "YES" ]; then
+ echo "This script is intended to prepare the directory for publishing"
+ echo "and replaces files. If you only want to build the UI run \`npm run build-ui\`."
+ echo "Otherwise set IS_PUBLISHING to \"YES\""
+ exit 1
+fi
+
+set -x
+
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+cd "$REPO_ROOT"
+
+rm -rf dist || true
+tsc --project tsconfig.publish.json
+# replace tsx with node for the new index.js
+sed -ie '1s/tsx/node/' dist/index.js
+# ensure it's executable
+chmod +x dist/index.js
+# move the ts source
+mv src src-old
+# move the built source
+mv dist/src dist/index.js dist/index.d.ts .
+# copy back unchanged ui code
+# could probably drop this as the ui code shouldn't really be imported from
+# the main package but keep for compat until split out.
+mv src-old/ui src/ui
+rm -rf src-old index.ts dist
diff --git a/scripts/undo-build.sh b/scripts/undo-build.sh
new file mode 100755
index 000000000..998123e09
--- /dev/null
+++ b/scripts/undo-build.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euxo pipefail
+
+# Undo what was done by build-for-publish.sh in the event this was ran locally
+
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+cd "$REPO_ROOT"
+
+rm -rf dist index.js index.d.ts || true
+git checkout src index.ts
+git clean -f src
diff --git a/src/config/ConfigLoader.ts b/src/config/ConfigLoader.ts
new file mode 100644
index 000000000..80429e382
--- /dev/null
+++ b/src/config/ConfigLoader.ts
@@ -0,0 +1,419 @@
+import fs from 'fs';
+import path from 'path';
+import axios from 'axios';
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+import EventEmitter from 'events';
+import envPaths from 'env-paths';
+
+const execFileAsync = promisify(execFile);
+
+interface GitAuth {
+ type: 'ssh';
+ privateKeyPath: string;
+}
+
+interface HttpAuth {
+ type: 'bearer';
+ token: string;
+}
+
+interface BaseSource {
+ type: 'file' | 'http' | 'git';
+ enabled: boolean;
+}
+
+interface FileSource extends BaseSource {
+ type: 'file';
+ path: string;
+}
+
+interface HttpSource extends BaseSource {
+ type: 'http';
+ url: string;
+ headers?: Record;
+ auth?: HttpAuth;
+}
+
+interface GitSource extends BaseSource {
+ type: 'git';
+ repository: string;
+ branch?: string;
+ path: string;
+ auth?: GitAuth;
+}
+
+type ConfigurationSource = FileSource | HttpSource | GitSource;
+
+export interface ConfigurationSources {
+ enabled: boolean;
+ sources: ConfigurationSource[];
+ reloadIntervalSeconds: number;
+ merge?: boolean;
+}
+
+export interface Configuration {
+ configurationSources: ConfigurationSources;
+ [key: string]: any;
+}
+
+// Add path validation helper
+function isValidPath(filePath: string): boolean {
+ if (!filePath || typeof filePath !== 'string') return false;
+
+ // Check for null bytes and other control characters
+ if (/[\0]/.test(filePath)) return false;
+
+ try {
+ path.resolve(filePath);
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
+// Add URL validation helper
+function isValidGitUrl(url: string): boolean {
+ // Allow git://, https://, or ssh:// URLs
+ // Also allow scp-style URLs (user@host:path)
+ const validUrlPattern =
+ /^(git:\/\/|https:\/\/|ssh:\/\/|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}:)/;
+ return typeof url === 'string' && validUrlPattern.test(url);
+}
+
+// Add branch name validation helper
+function isValidBranchName(branch: string): boolean {
+ if (typeof branch !== 'string') return false;
+
+ // Check for consecutive dots
+ if (branch.includes('..')) return false;
+
+ // Check other branch name rules
+ // Branch names can contain alphanumeric, -, _, /, and .
+ // Cannot start with - or .
+ // Cannot contain consecutive dots
+ // Cannot contain control characters or spaces
+ const validBranchPattern = /^[a-zA-Z0-9][a-zA-Z0-9_/.-]*$/;
+ return validBranchPattern.test(branch);
+}
+
+export class ConfigLoader extends EventEmitter {
+ private config: Configuration;
+ private reloadTimer: NodeJS.Timeout | null;
+ private isReloading: boolean;
+ private cacheDir: string | null;
+
+ constructor(initialConfig: Configuration) {
+ super();
+ this.config = initialConfig;
+ this.reloadTimer = null;
+ this.isReloading = false;
+ this.cacheDir = null;
+ }
+
+ async initialize(): Promise {
+ // Get cache directory path
+ const paths = envPaths('git-proxy');
+ this.cacheDir = paths.cache;
+
+ // Create cache directory if it doesn't exist
+ if (!fs.existsSync(this.cacheDir)) {
+ try {
+ fs.mkdirSync(this.cacheDir, { recursive: true });
+ console.log(`Created cache directory at ${this.cacheDir}`);
+ return true;
+ } catch (err) {
+ console.error('Failed to create cache directory:', err);
+ return false;
+ }
+ }
+ console.log(`Using cache directory at ${this.cacheDir}`);
+ return true;
+ }
+
+ async start(): Promise {
+ const { configurationSources } = this.config;
+ if (!configurationSources?.enabled) {
+ console.log('Configuration sources are disabled');
+ return;
+ }
+
+ console.log('Configuration sources are enabled');
+ console.log(
+ `Sources: ${JSON.stringify(configurationSources.sources.filter((s: ConfigurationSource) => s.enabled).map((s: ConfigurationSource) => s.type))}`,
+ );
+
+ // Clear any existing interval before starting a new one
+ if (this.reloadTimer) {
+ clearInterval(this.reloadTimer);
+ this.reloadTimer = null;
+ }
+
+ // Start periodic reload if interval is set
+ if (configurationSources.reloadIntervalSeconds > 0) {
+ console.log(
+ `Setting reload interval to ${configurationSources.reloadIntervalSeconds} seconds`,
+ );
+ this.reloadTimer = setInterval(
+ () => this.reloadConfiguration(),
+ configurationSources.reloadIntervalSeconds * 1000,
+ );
+ }
+
+ // Do initial load
+ await this.reloadConfiguration();
+ }
+
+ stop(): void {
+ if (this.reloadTimer) {
+ clearInterval(this.reloadTimer);
+ this.reloadTimer = null;
+ }
+ }
+
+ async reloadConfiguration(): Promise {
+ if (this.isReloading) {
+ console.log('Configuration reload already in progress, skipping');
+ return;
+ }
+ this.isReloading = true;
+ console.log('Starting configuration reload');
+
+ try {
+ const { configurationSources } = this.config;
+ if (!configurationSources?.enabled) {
+ console.log('Configuration sources are disabled, skipping reload');
+ return;
+ }
+
+ const enabledSources = configurationSources.sources.filter(
+ (source: ConfigurationSource) => source.enabled,
+ );
+ console.log(`Found ${enabledSources.length} enabled configuration sources`);
+
+ const configs = await Promise.all(
+ enabledSources.map(async (source: ConfigurationSource) => {
+ try {
+ console.log(`Loading configuration from ${source.type} source`);
+ return await this.loadFromSource(source);
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ console.error(`Error loading from ${source.type} source:`, error.message);
+ }
+ return null;
+ }
+ }),
+ );
+
+ // Filter out null results from failed loads
+ const validConfigs = configs.filter((config): config is Configuration => config !== null);
+
+ if (validConfigs.length === 0) {
+ console.log('No valid configurations loaded from any source');
+ return;
+ }
+
+ // Use merge strategy based on configuration
+ const shouldMerge = configurationSources.merge ?? true; // Default to true for backward compatibility
+ console.log(`Using ${shouldMerge ? 'merge' : 'override'} strategy for configuration`);
+
+ const newConfig = shouldMerge
+ ? validConfigs.reduce(
+ (acc, curr) => {
+ return this.deepMerge(acc, curr) as Configuration;
+ },
+ { ...this.config },
+ )
+ : { ...this.config, ...validConfigs[validConfigs.length - 1] }; // Use last config for override
+
+ // Emit change event if config changed
+ if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) {
+ console.log('Configuration has changed, updating and emitting change event');
+ this.config = newConfig;
+ this.emit('configurationChanged', this.config);
+ } else {
+ console.log('Configuration has not changed, no update needed');
+ }
+ } catch (error: unknown) {
+ console.error('Error reloading configuration:', error);
+ this.emit('configurationError', error);
+ } finally {
+ this.isReloading = false;
+ }
+ }
+
+ async loadFromSource(source: ConfigurationSource): Promise {
+ let exhaustiveCheck: never;
+ switch (source.type) {
+ case 'file':
+ return this.loadFromFile(source as FileSource);
+ case 'http':
+ return this.loadFromHttp(source as HttpSource);
+ case 'git':
+ return this.loadFromGit(source as GitSource);
+ default:
+ exhaustiveCheck = source;
+ throw new Error(`Unsupported configuration source type: ${exhaustiveCheck}`);
+ }
+ }
+
+ async loadFromFile(source: FileSource): Promise {
+ const configPath = path.resolve(process.cwd(), source.path);
+ if (!isValidPath(configPath)) {
+ throw new Error('Invalid configuration file path');
+ }
+ console.log(`Loading configuration from file: ${configPath}`);
+ const content = await fs.promises.readFile(configPath, 'utf8');
+ return JSON.parse(content);
+ }
+
+ async loadFromHttp(source: HttpSource): Promise {
+ console.log(`Loading configuration from HTTP: ${source.url}`);
+ const headers = {
+ ...source.headers,
+ ...(source.auth?.type === 'bearer' ? { Authorization: `Bearer ${source.auth.token}` } : {}),
+ };
+
+ const response = await axios.get(source.url, { headers });
+ return response.data;
+ }
+
+ async loadFromGit(source: GitSource): Promise {
+ console.log(`Loading configuration from Git: ${source.repository}`);
+
+ // Validate inputs
+ if (!source.repository || !isValidGitUrl(source.repository)) {
+ throw new Error('Invalid repository URL format');
+ }
+ if (source.branch && !isValidBranchName(source.branch)) {
+ throw new Error('Invalid branch name format');
+ }
+
+ // Use OS-specific cache directory
+ const paths = envPaths('git-proxy', { suffix: '' });
+ const tempDir = path.join(paths.cache, 'git-config-cache');
+
+ if (!isValidPath(tempDir)) {
+ throw new Error('Invalid temporary directory path');
+ }
+
+ console.log(`Creating git cache directory at ${tempDir}`);
+ await fs.promises.mkdir(tempDir, { recursive: true });
+
+ // Create a safe directory name from the repository URL
+ const repoDirName = Buffer.from(source.repository)
+ .toString('base64')
+ .replace(/[^a-zA-Z0-9]/g, '_');
+ const repoDir = path.join(tempDir, repoDirName);
+
+ if (!isValidPath(repoDir)) {
+ throw new Error('Invalid repository directory path');
+ }
+
+ console.log(`Using repository directory: ${repoDir}`);
+
+ // Clone or pull repository
+ if (!fs.existsSync(repoDir)) {
+ console.log(`Cloning repository ${source.repository} to ${repoDir}`);
+ const execOptions = {
+ cwd: process.cwd(),
+ env: {
+ ...process.env,
+ ...(source.auth?.type === 'ssh'
+ ? {
+ GIT_SSH_COMMAND: `ssh -i ${source.auth.privateKeyPath}`,
+ }
+ : {}),
+ },
+ };
+
+ try {
+ await execFileAsync('git', ['clone', source.repository, repoDir], execOptions);
+ console.log('Repository cloned successfully');
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ console.error('Failed to clone repository:', error.message);
+ throw new Error(`Failed to clone repository: ${error.message}`);
+ }
+ throw error;
+ }
+ } else {
+ console.log(`Pulling latest changes from ${source.repository}`);
+ try {
+ await execFileAsync('git', ['pull'], { cwd: repoDir });
+ console.log('Repository pulled successfully');
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ console.error('Failed to pull repository:', error.message);
+ throw new Error(`Failed to pull repository: ${error.message}`);
+ }
+ throw error;
+ }
+ }
+
+ // Checkout specific branch if specified
+ if (source.branch) {
+ console.log(`Checking out branch: ${source.branch}`);
+ try {
+ await execFileAsync('git', ['checkout', source.branch], { cwd: repoDir });
+ console.log(`Branch ${source.branch} checked out successfully`);
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ console.error(`Failed to checkout branch ${source.branch}:`, error.message);
+ throw new Error(`Failed to checkout branch ${source.branch}: ${error.message}`);
+ }
+ throw error;
+ }
+ }
+
+ // Read and parse config file
+ const configPath = path.join(repoDir, source.path);
+ if (!isValidPath(configPath)) {
+ throw new Error('Invalid configuration file path in repository');
+ }
+
+ console.log(`Reading configuration file: ${configPath}`);
+ if (!fs.existsSync(configPath)) {
+ throw new Error(`Configuration file not found at ${configPath}`);
+ }
+
+ try {
+ const content = await fs.promises.readFile(configPath, 'utf8');
+ const config = JSON.parse(content);
+ console.log('Configuration loaded successfully from Git');
+ return config;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ console.error('Failed to read or parse configuration file:', error.message);
+ throw new Error(`Failed to read or parse configuration file: ${error.message}`);
+ }
+ throw error;
+ }
+ }
+
+ deepMerge(target: Record, source: Record): Record {
+ const output = { ...target };
+ if (isObject(target) && isObject(source)) {
+ Object.keys(source).forEach((key) => {
+ if (isObject(source[key])) {
+ if (!(key in target)) {
+ Object.assign(output, { [key]: source[key] });
+ } else {
+ output[key] = this.deepMerge(target[key], source[key]);
+ }
+ } else {
+ Object.assign(output, { [key]: source[key] });
+ }
+ });
+ }
+ return output;
+ }
+}
+
+// Helper function to check if a value is an object
+function isObject(item: unknown): item is Record {
+ return item !== null && typeof item === 'object' && !Array.isArray(item);
+}
+
+export default ConfigLoader;
+export { isValidGitUrl, isValidPath, isValidBranchName };
diff --git a/src/config/file.ts b/src/config/file.ts
index e7aadcd46..7affe0d5a 100644
--- a/src/config/file.ts
+++ b/src/config/file.ts
@@ -1,27 +1,43 @@
import { readFileSync } from 'fs';
import { join } from 'path';
-import { validate as jsonSchemaValidate } from 'jsonschema';
+import { ConfigSchema, type Config } from '../../proxy.config.schema';
-export let configFile: string = join(process.cwd(), 'proxy.config.json');
+export let configFile: string = join(process.cwd(), 'config.proxy.json');
+export let config: Config;
/**
- * Set the config file path.
- * @param {string} file - The path to the config file.
+ * Sets the path to the configuration file.
+ *
+ * @param {string} file - The path to the configuration file.
+ * @return {void}
*/
export function setConfigFile(file: string) {
configFile = file;
}
/**
- * Validate config file.
- * @param {string} configFilePath - The path to the config file.
- * @return {boolean} - Returns true if validation is successful.
- * @throws Will throw an error if the validation fails.
+ * Loads and validates the configuration file using Zod.
+ * If validation succeeds, the parsed config is stored in the exported `config`.
+ *
+ * @return {Config} The validated and default-filled configuration object.
+ * @throws {ZodError} If validation fails.
*/
-export function validate(configFilePath: string = configFile!): boolean {
- const config = JSON.parse(readFileSync(configFilePath, 'utf-8'));
- const schemaPath = join(process.cwd(), 'config.schema.json');
- const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
- jsonSchemaValidate(config, schema, { required: true, throwError: true });
+export function loadConfig(): Config {
+ const raw = JSON.parse(readFileSync(configFile, 'utf-8'));
+ const parsed = ConfigSchema.parse(raw);
+ config = parsed;
+ return parsed;
+}
+
+/**
+ * Validates a configuration file without mutating the exported `config`.
+ *
+ * @param {string} [filePath=configFile] - Path to the configuration file to validate.
+ * @return {boolean} Returns `true` if the file passes validation.
+ * @throws {ZodError} If validation fails.
+ */
+export function validate(filePath: string = configFile): boolean {
+ const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
+ ConfigSchema.parse(raw);
return true;
}
diff --git a/src/config/index.ts b/src/config/index.ts
index 782c75564..b92134d75 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -1,11 +1,13 @@
import { existsSync, readFileSync } from 'fs';
import defaultSettings from '../../proxy.config.json';
-import { configFile } from './file';
+import { configFile, validate } from './file';
+import { ConfigLoader, Configuration } from './ConfigLoader';
import {
Authentication,
AuthorisedRepo,
Database,
+ RateLimitConfig,
TempPasswordConfig,
UserSettings,
} from './types';
@@ -30,11 +32,19 @@ let _urlShortener: string = defaultSettings.urlShortener;
let _contactEmail: string = defaultSettings.contactEmail;
let _csrfProtection: boolean = defaultSettings.csrfProtection;
let _domains: Record = defaultSettings.domains;
+let _rateLimit: RateLimitConfig = defaultSettings.rateLimit;
+
// These are not always present in the default config file, so casting is required
let _tlsEnabled = defaultSettings.tls.enabled;
let _tlsKeyPemPath = defaultSettings.tls.key;
let _tlsCertPemPath = defaultSettings.tls.cert;
+// Initialize configuration with defaults and user settings
+let _config = { ...defaultSettings, ...(_userSettings || {}) } as Configuration;
+
+// Create config loader instance
+const configLoader = new ConfigLoader(_config);
+
// Get configured proxy URL
export const getProxyUrl = () => {
if (_userSettings !== null && _userSettings.proxyUrl) {
@@ -99,6 +109,7 @@ export const logConfiguration = () => {
console.log(`authorisedList = ${JSON.stringify(getAuthorisedList())}`);
console.log(`data sink = ${JSON.stringify(getDatabase())}`);
console.log(`authentication = ${JSON.stringify(getAuthentication())}`);
+ console.log(`rateLimit = ${JSON.stringify(getRateLimit())}`);
};
export const getAPIs = () => {
@@ -217,3 +228,62 @@ export const getDomains = () => {
}
return _domains;
};
+
+export const getRateLimit = () => {
+ if (_userSettings && _userSettings.rateLimit) {
+ _rateLimit = _userSettings.rateLimit;
+ }
+ return _rateLimit;
+};
+
+// Function to handle configuration updates
+const handleConfigUpdate = async (newConfig: typeof _config) => {
+ console.log('Configuration updated from external source');
+ try {
+ // 1. Get proxy module dynamically to avoid circular dependency
+ const proxy = require('../proxy');
+
+ // 2. Stop existing services
+ await proxy.stop();
+
+ // 3. Update config
+ _config = newConfig;
+
+ // 4. Validate new configuration
+ validate();
+
+ // 5. Restart services with new config
+ await proxy.start();
+
+ console.log('Services restarted with new configuration');
+ } catch (error) {
+ console.error('Failed to apply new configuration:', error);
+ // Attempt to restart with previous config
+ try {
+ const proxy = require('../proxy');
+ await proxy.start();
+ } catch (startError) {
+ console.error('Failed to restart services:', startError);
+ }
+ }
+};
+
+// Handle configuration updates
+configLoader.on('configurationChanged', handleConfigUpdate);
+
+configLoader.on('configurationError', (error: Error) => {
+ console.error('Error loading external configuration:', error);
+});
+
+// Start the config loader if external sources are enabled
+configLoader.start().catch((error: Error) => {
+ console.error('Failed to start configuration loader:', error);
+});
+
+// Force reload of configuration
+const reloadConfiguration = async () => {
+ await configLoader.reloadConfiguration();
+};
+
+// Export reloadConfiguration
+export { reloadConfiguration };
diff --git a/src/config/types.ts b/src/config/types.ts
index 30428f232..a1907477a 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -1,3 +1,5 @@
+import { Options as RateLimitOptions } from 'express-rate-limit';
+
export interface UserSettings {
authorisedList: AuthorisedRepo[];
sink: Database[];
@@ -18,6 +20,7 @@ export interface UserSettings {
contactEmail: string;
csrfProtection: boolean;
domains: Record;
+ rateLimit: RateLimitConfig;
}
export interface TLSConfig {
@@ -50,3 +53,7 @@ export interface TempPasswordConfig {
sendEmail: boolean;
emailConfig: Record;
}
+
+export type RateLimitConfig = Partial<
+ Pick
+>;
diff --git a/src/proxy/chain.ts b/src/proxy/chain.ts
index 41a7cc495..8bc5e3120 100644
--- a/src/proxy/chain.ts
+++ b/src/proxy/chain.ts
@@ -14,12 +14,16 @@ const pushActionChain: ((req: any, action: Action) => Promise)[] = [
proc.push.writePack,
proc.push.preReceive,
proc.push.getDiff,
+ // run before clear remote
+ proc.push.gitleaks,
proc.push.clearBareClone,
proc.push.scanDiff,
proc.push.blockForAuth,
];
-const pullActionChain: ((req: any, action: Action) => Promise)[] = [proc.push.checkRepoInAuthorisedList];
+const pullActionChain: ((req: any, action: Action) => Promise)[] = [
+ proc.push.checkRepoInAuthorisedList,
+];
let pluginsInserted = false;
@@ -57,7 +61,9 @@ export const executeChain = async (req: any, res: any): Promise => {
*/
let chainPluginLoader: PluginLoader;
-const getChain = async (action: Action): Promise<((req: any, action: Action) => Promise)[]> => {
+export const getChain = async (
+ action: Action,
+): Promise<((req: any, action: Action) => Promise)[]> => {
if (chainPluginLoader === undefined) {
console.error(
'Plugin loader was not initialized! This is an application error. Please report it to the GitProxy maintainers. Skipping plugins...',
diff --git a/src/proxy/index.ts b/src/proxy/index.ts
index 04fce52d7..4cfcda986 100644
--- a/src/proxy/index.ts
+++ b/src/proxy/index.ts
@@ -1,4 +1,4 @@
-import express from 'express';
+import express, { Application } from 'express';
import bodyParser from 'body-parser';
import http from 'http';
import https from 'https';
@@ -19,7 +19,15 @@ import { Repo } from '../db/types';
const { GIT_PROXY_SERVER_PORT: proxyHttpPort, GIT_PROXY_HTTPS_SERVER_PORT: proxyHttpsPort } =
require('../config/env').serverConfig;
-const options = {
+interface ServerOptions {
+ inflate: boolean;
+ limit: string;
+ type: string;
+ key: Buffer | undefined;
+ cert: Buffer | undefined;
+}
+
+const options: ServerOptions = {
inflate: true,
limit: '100000kb',
type: '*/*',
@@ -27,7 +35,7 @@ const options = {
cert: getTLSEnabled() ? fs.readFileSync(getTLSCertPemPath()) : undefined,
};
-const proxyPreparations = async () => {
+export const proxyPreparations = async () => {
const plugins = getPlugins();
const pluginLoader = new PluginLoader(plugins);
await pluginLoader.load();
@@ -47,7 +55,7 @@ const proxyPreparations = async () => {
};
// just keep this async incase it needs async stuff in the future
-const createApp = async () => {
+const createApp = async (): Promise => {
const app = express();
// Setup the proxy middleware
app.use(bodyParser.raw(options));
@@ -55,23 +63,53 @@ const createApp = async () => {
return app;
};
-const start = async () => {
+let httpServer: http.Server | null = null;
+let httpsServer: https.Server | null = null;
+
+const start = async (): Promise => {
const app = await createApp();
await proxyPreparations();
- http.createServer(options as any, app).listen(proxyHttpPort, () => {
+ httpServer = http.createServer(options as any, app).listen(proxyHttpPort, () => {
console.log(`HTTP Proxy Listening on ${proxyHttpPort}`);
});
// Start HTTPS server only if TLS is enabled
if (getTLSEnabled()) {
- https.createServer(options, app).listen(proxyHttpsPort, () => {
+ httpsServer = https.createServer(options, app).listen(proxyHttpsPort, () => {
console.log(`HTTPS Proxy Listening on ${proxyHttpsPort}`);
});
}
return app;
};
+const stop = (): Promise => {
+ return new Promise((resolve, reject) => {
+ try {
+ // Close HTTP server if it exists
+ if (httpServer) {
+ httpServer.close(() => {
+ console.log('HTTP server closed');
+ httpServer = null;
+ });
+ }
+
+ // Close HTTPS server if it exists
+ if (httpsServer) {
+ httpsServer.close(() => {
+ console.log('HTTPS server closed');
+ httpsServer = null;
+ });
+ }
+
+ resolve();
+ } catch (error) {
+ reject(error);
+ }
+ });
+};
+
export default {
proxyPreparations,
createApp,
start,
+ stop,
};
diff --git a/src/proxy/processors/push-action/checkCommitMessages.ts b/src/proxy/processors/push-action/checkCommitMessages.ts
index 577a572af..7a95f6c12 100644
--- a/src/proxy/processors/push-action/checkCommitMessages.ts
+++ b/src/proxy/processors/push-action/checkCommitMessages.ts
@@ -70,7 +70,7 @@ const exec = async (req: any, action: Action): Promise => {
step.error = true;
step.log(`The following commit messages are illegal: ${illegalMessages}`);
step.setError(
- '\n\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\n\n',
+ `\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\nThe following commit messages are illegal: ${JSON.stringify(illegalMessages)}\n\n`,
);
action.addStep(step);
diff --git a/src/proxy/processors/push-action/gitleaks.ts b/src/proxy/processors/push-action/gitleaks.ts
new file mode 100644
index 000000000..73aabe550
--- /dev/null
+++ b/src/proxy/processors/push-action/gitleaks.ts
@@ -0,0 +1,184 @@
+import { Action, Step } from '../../actions';
+import { getAPIs } from '../../../config';
+import { spawn } from 'node:child_process';
+import fs from 'node:fs/promises';
+import { PathLike } from 'node:fs';
+
+const EXIT_CODE = 99;
+
+function runCommand(
+ cwd: string,
+ command: string,
+ args: readonly string[] = [],
+): Promise<{
+ exitCode: number | null;
+ stdout: string;
+ stderr: string;
+}> {
+ return new Promise((resolve, reject) => {
+ const child = spawn(command, args, { cwd, shell: true });
+
+ let stdout = '';
+ let stderr = '';
+
+ child.stdout.on('data', (data) => {
+ stdout += data?.toString() ?? '';
+ });
+
+ child.stderr.on('data', (data) => {
+ stderr += data?.toString() ?? '';
+ });
+
+ child.on('close', (exitCode) => {
+ resolve({ exitCode, stdout, stderr });
+ });
+
+ child.on('error', (err) => {
+ reject(err);
+ });
+ });
+}
+
+type ConfigOptions = {
+ enabled: boolean;
+ ignoreGitleaksAllow: boolean;
+ noColor: boolean;
+ configPath: string | undefined;
+};
+
+const DEFAULT_CONFIG: ConfigOptions = {
+ // adding gitleaks into main git-proxy for now as default off
+ // in the future will likely be moved to a plugin where it'll be default on
+ enabled: false,
+ ignoreGitleaksAllow: true,
+ noColor: false,
+ configPath: undefined,
+};
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null;
+}
+
+async function fileIsReadable(path: PathLike): Promise {
+ try {
+ if (!(await fs.stat(path)).isFile()) {
+ return false;
+ }
+ await fs.access(path, fs.constants.R_OK);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+const getPluginConfig = async (): Promise => {
+ const userConfig = getAPIs();
+ if (typeof userConfig !== 'object') {
+ return DEFAULT_CONFIG;
+ }
+ if (!Object.hasOwn(userConfig, 'gitleaks')) {
+ return DEFAULT_CONFIG;
+ }
+ const gitleaksConfig = userConfig.gitleaks;
+ if (!isRecord(gitleaksConfig)) {
+ return DEFAULT_CONFIG;
+ }
+
+ let configPath: string | undefined = undefined;
+ if (typeof gitleaksConfig.configPath === 'string') {
+ const userConfigPath = gitleaksConfig.configPath.trim();
+ if (userConfigPath.length > 0 && (await fileIsReadable(userConfigPath))) {
+ configPath = userConfigPath;
+ } else {
+ console.error('could not read file at the config path provided, will not be fed to gitleaks');
+ throw new Error("could not check user's config path");
+ }
+ }
+
+ // TODO: integrate zod
+ return {
+ enabled:
+ typeof gitleaksConfig.enabled === 'boolean' ? gitleaksConfig.enabled : DEFAULT_CONFIG.enabled,
+ ignoreGitleaksAllow:
+ typeof gitleaksConfig.ignoreGitleaksAllow === 'boolean'
+ ? gitleaksConfig.ignoreGitleaksAllow
+ : DEFAULT_CONFIG.ignoreGitleaksAllow,
+ noColor:
+ typeof gitleaksConfig.noColor === 'boolean' ? gitleaksConfig.noColor : DEFAULT_CONFIG.noColor,
+ configPath,
+ };
+};
+
+const exec = async (req: any, action: Action): Promise => {
+ const step = new Step('gitleaks');
+
+ let config: ConfigOptions | undefined = undefined;
+ try {
+ config = await getPluginConfig();
+ } catch (e) {
+ console.error('failed to get gitleaks config, please fix the error:', e);
+ action.error = true;
+ step.setError('failed setup gitleaks, please contact an administrator\n');
+ action.addStep(step);
+ return action;
+ }
+
+ const { commitFrom, commitTo } = action;
+ const workingDir = `${action.proxyGitPath}/${action.repoName}`;
+ console.log(`Scanning range with gitleaks: ${commitFrom}:${commitTo}`, workingDir);
+
+ try {
+ const gitRootCommit = await runCommand(workingDir, 'git', [
+ 'rev-list',
+ '--max-parents=0',
+ 'HEAD',
+ ]);
+ if (gitRootCommit.exitCode !== 0) {
+ throw new Error('failed to run git');
+ }
+ const rootCommit = gitRootCommit.stdout.trim();
+
+ const gitleaksArgs = [
+ `--exit-code=${EXIT_CODE}`,
+ '--platform=none',
+ config.configPath ? `--config=${config.configPath}` : undefined, // allow for custom config
+ config.ignoreGitleaksAllow ? '--ignore-gitleaks-allow' : undefined, // force scanning for security
+ '--no-banner', // reduce git-proxy error output
+ config.noColor ? '--no-color' : undefined, // colour output should appear properly in the console
+ '--redact', // avoid printing the contents
+ '--verbose',
+ 'git',
+ // not using --no-merges to be sure we're scanning the diff
+ // only add ^ if the commitFrom isn't the repo's rootCommit
+ `--log-opts='--first-parent ${rootCommit === commitFrom ? rootCommit : `${commitFrom}^`}..${commitTo}'`,
+ ].filter((v) => typeof v === 'string');
+ const gitleaks = await runCommand(workingDir, 'gitleaks', gitleaksArgs);
+
+ if (gitleaks.exitCode !== 0) {
+ // any failure
+ step.error = true;
+ if (gitleaks.exitCode !== EXIT_CODE) {
+ step.setError('failed to run gitleaks, please contact an administrator\n');
+ } else {
+ // exit code matched our gitleaks findings exit code
+ // newline prefix to avoid tab indent at the start
+ step.setError('\n' + gitleaks.stdout + gitleaks.stderr);
+ }
+ } else {
+ console.log('succeded');
+ console.log(gitleaks.stderr);
+ }
+ } catch (e) {
+ action.error = true;
+ step.setError('failed to spawn gitleaks, please contact an administrator\n');
+ action.addStep(step);
+ return action;
+ }
+
+ action.addStep(step);
+ return action;
+};
+
+exec.displayName = 'gitleaks.exec';
+
+export { exec };
diff --git a/src/proxy/processors/push-action/index.ts b/src/proxy/processors/push-action/index.ts
index 9fc2065f7..704e6febf 100644
--- a/src/proxy/processors/push-action/index.ts
+++ b/src/proxy/processors/push-action/index.ts
@@ -5,6 +5,7 @@ import { exec as audit } from './audit';
import { exec as pullRemote } from './pullRemote';
import { exec as writePack } from './writePack';
import { exec as getDiff } from './getDiff';
+import { exec as gitleaks } from './gitleaks';
import { exec as scanDiff } from './scanDiff';
import { exec as blockForAuth } from './blockForAuth';
import { exec as checkIfWaitingAuth } from './checkIfWaitingAuth';
@@ -21,6 +22,7 @@ export {
pullRemote,
writePack,
getDiff,
+ gitleaks,
scanDiff,
blockForAuth,
checkIfWaitingAuth,
diff --git a/src/service/index.js b/src/service/index.js
index d384fcd6e..02e416aa0 100644
--- a/src/service/index.js
+++ b/src/service/index.js
@@ -8,11 +8,10 @@ const config = require('../config');
const db = require('../db');
const rateLimit = require('express-rate-limit');
const lusca = require('lusca');
+const configLoader = require('../config/ConfigLoader');
+const proxy = require('../proxy');
-const limiter = rateLimit({
- windowMs: 15 * 60 * 1000, // 15 minutes
- max: 100, // limit each IP to 100 requests per windowMs
-});
+const limiter = rateLimit(config.getRateLimit());
const { GIT_PROXY_UI_PORT: uiPort } = require('../config/env').serverConfig;
@@ -32,6 +31,42 @@ const createApp = async () => {
app.use(cors(corsOptions));
app.set('trust proxy', 1);
app.use(limiter);
+
+ // Add new admin-only endpoint to reload config
+ app.post('/api/v1/admin/reload-config', async (req, res) => {
+ if (!req.isAuthenticated() || !req.user.admin) {
+ return res.status(403).json({ error: 'Unauthorized' });
+ }
+
+ try {
+ // 1. Reload configuration
+ await configLoader.loadConfiguration();
+
+ // 2. Stop existing services
+ await proxy.stop();
+
+ // 3. Apply new configuration
+ config.validate();
+
+ // 4. Restart services with new config
+ await proxy.start();
+
+ console.log('Configuration reloaded and services restarted successfully');
+ res.json({ status: 'success', message: 'Configuration reloaded and services restarted' });
+ } catch (error) {
+ console.error('Failed to reload configuration and restart services:', error);
+
+ // Attempt to restart with existing config if reload fails
+ try {
+ await proxy.start();
+ } catch (startError) {
+ console.error('Failed to restart services:', startError);
+ }
+
+ res.status(500).json({ error: 'Failed to reload configuration' });
+ }
+ });
+
app.use(
session({
store: config.getDatabase().type === 'mongo' ? db.getSessionStore(session) : null,
diff --git a/test/ConfigLoader.test.js b/test/ConfigLoader.test.js
new file mode 100644
index 000000000..59d63458a
--- /dev/null
+++ b/test/ConfigLoader.test.js
@@ -0,0 +1,426 @@
+import fs from 'fs';
+import path from 'path';
+import { expect } from 'chai';
+import { ConfigLoader } from '../src/config/ConfigLoader';
+import { isValidGitUrl, isValidPath, isValidBranchName } from '../src/config/ConfigLoader';
+import sinon from 'sinon';
+import axios from 'axios';
+
+describe('ConfigLoader', () => {
+ let configLoader;
+ let tempDir;
+ let tempConfigFile;
+
+ beforeEach(() => {
+ // Create temp directory for test files
+ tempDir = fs.mkdtempSync('gitproxy-configloader-test-');
+ tempConfigFile = path.join(tempDir, 'test-config.json');
+ });
+
+ afterEach(() => {
+ // Clean up temp files
+ if (fs.existsSync(tempDir)) {
+ fs.rmSync(tempDir, { recursive: true });
+ }
+ sinon.restore();
+ });
+
+ describe('loadFromFile', () => {
+ it('should load configuration from file', async () => {
+ const testConfig = {
+ proxyUrl: 'https://test.com',
+ cookieSecret: 'test-secret',
+ };
+ fs.writeFileSync(tempConfigFile, JSON.stringify(testConfig));
+
+ configLoader = new ConfigLoader({});
+ const result = await configLoader.loadFromFile({
+ type: 'file',
+ enabled: true,
+ path: tempConfigFile,
+ });
+
+ expect(result).to.deep.equal(testConfig);
+ });
+ });
+
+ describe('loadFromHttp', () => {
+ it('should load configuration from HTTP endpoint', async () => {
+ const testConfig = {
+ proxyUrl: 'https://test.com',
+ cookieSecret: 'test-secret',
+ };
+
+ sinon.stub(axios, 'get').resolves({ data: testConfig });
+
+ configLoader = new ConfigLoader({});
+ const result = await configLoader.loadFromHttp({
+ type: 'http',
+ enabled: true,
+ url: 'http://config-service/config',
+ headers: {},
+ });
+
+ expect(result).to.deep.equal(testConfig);
+ });
+
+ it('should include bearer token if provided', async () => {
+ const axiosStub = sinon.stub(axios, 'get').resolves({ data: {} });
+
+ configLoader = new ConfigLoader({});
+ await configLoader.loadFromHttp({
+ type: 'http',
+ enabled: true,
+ url: 'http://config-service/config',
+ auth: {
+ type: 'bearer',
+ token: 'test-token',
+ },
+ });
+
+ expect(
+ axiosStub.calledWith('http://config-service/config', {
+ headers: { Authorization: 'Bearer test-token' },
+ }),
+ ).to.be.true;
+ });
+ });
+
+ describe('reloadConfiguration', () => {
+ it('should emit configurationChanged event when config changes', async () => {
+ const initialConfig = {
+ configurationSources: {
+ enabled: true,
+ sources: [
+ {
+ type: 'file',
+ enabled: true,
+ path: tempConfigFile,
+ },
+ ],
+ reloadIntervalSeconds: 0,
+ },
+ };
+
+ const newConfig = {
+ proxyUrl: 'https://new-test.com',
+ };
+
+ fs.writeFileSync(tempConfigFile, JSON.stringify(newConfig));
+
+ configLoader = new ConfigLoader(initialConfig);
+ const spy = sinon.spy();
+ configLoader.on('configurationChanged', spy);
+
+ await configLoader.reloadConfiguration();
+
+ expect(spy.calledOnce).to.be.true;
+ expect(spy.firstCall.args[0]).to.deep.include(newConfig);
+ });
+
+ it('should not emit event if config has not changed', async () => {
+ const testConfig = {
+ proxyUrl: 'https://test.com',
+ };
+
+ const config = {
+ configurationSources: {
+ enabled: true,
+ sources: [
+ {
+ type: 'file',
+ enabled: true,
+ path: tempConfigFile,
+ },
+ ],
+ reloadIntervalSeconds: 0,
+ },
+ };
+
+ fs.writeFileSync(tempConfigFile, JSON.stringify(testConfig));
+
+ configLoader = new ConfigLoader(config);
+ const spy = sinon.spy();
+ configLoader.on('configurationChanged', spy);
+
+ await configLoader.reloadConfiguration(); // First reload should emit
+ await configLoader.reloadConfiguration(); // Second reload should not emit since config hasn't changed
+
+ expect(spy.calledOnce).to.be.true; // Should only emit once
+ });
+ });
+
+ describe('initialize', () => {
+ it('should initialize cache directory using env-paths', async () => {
+ const configLoader = new ConfigLoader({});
+ await configLoader.initialize();
+
+ // Check that cacheDir is set and is a string
+ expect(configLoader.cacheDir).to.be.a('string');
+
+ // Check that it contains 'git-proxy' in the path
+ expect(configLoader.cacheDir).to.include('git-proxy');
+
+ // On macOS, it should be in the Library/Caches directory
+ // On Linux, it should be in the ~/.cache directory
+ // On Windows, it should be in the AppData/Local directory
+ if (process.platform === 'darwin') {
+ expect(configLoader.cacheDir).to.include('Library/Caches');
+ } else if (process.platform === 'linux') {
+ expect(configLoader.cacheDir).to.include('.cache');
+ } else if (process.platform === 'win32') {
+ expect(configLoader.cacheDir).to.include('AppData/Local');
+ }
+ });
+
+ it('should create cache directory if it does not exist', async () => {
+ const configLoader = new ConfigLoader({});
+ await configLoader.initialize();
+
+ // Check if directory exists
+ expect(fs.existsSync(configLoader.cacheDir)).to.be.true;
+ });
+ });
+
+ describe('loadRemoteConfig', () => {
+ let configLoader;
+ beforeEach(async () => {
+ const configFilePath = path.join(__dirname, '..', 'proxy.config.json');
+ const config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'));
+
+ config.configurationSources.enabled = true;
+ configLoader = new ConfigLoader(config);
+ await configLoader.initialize();
+ });
+
+ it('should load configuration from git repository', async function () {
+ // eslint-disable-next-line no-invalid-this
+ this.timeout(10000);
+
+ const source = {
+ type: 'git',
+ repository: 'https://github.com/finos/git-proxy.git',
+ path: 'proxy.config.json',
+ branch: 'main',
+ enabled: true,
+ };
+
+ const config = await configLoader.loadFromGit(source);
+
+ // Verify the loaded config has expected structure
+ expect(config).to.be.an('object');
+ expect(config).to.have.property('proxyUrl');
+ expect(config).to.have.property('cookieSecret');
+ });
+
+ it('should throw error for invalid configuration file path', async function () {
+ const source = {
+ type: 'git',
+ repository: 'https://github.com/finos/git-proxy.git',
+ path: '\0', // Invalid path
+ branch: 'main',
+ enabled: true,
+ };
+
+ try {
+ await configLoader.loadFromGit(source);
+ throw new Error('Expected error was not thrown');
+ } catch (error) {
+ expect(error.message).to.equal('Invalid configuration file path in repository');
+ }
+ });
+
+ it('should load configuration from http', async function () {
+ // eslint-disable-next-line no-invalid-this
+ this.timeout(10000);
+
+ const source = {
+ type: 'http',
+ url: 'https://raw.githubusercontent.com/finos/git-proxy/refs/heads/main/proxy.config.json',
+ enabled: true,
+ };
+
+ const config = await configLoader.loadFromHttp(source);
+
+ // Verify the loaded config has expected structure
+ expect(config).to.be.an('object');
+ expect(config).to.have.property('proxyUrl');
+ expect(config).to.have.property('cookieSecret');
+ });
+ });
+
+ describe('deepMerge', () => {
+ let configLoader;
+
+ beforeEach(() => {
+ configLoader = new ConfigLoader({});
+ });
+
+ it('should merge simple objects', () => {
+ const target = { a: 1, b: 2 };
+ const source = { b: 3, c: 4 };
+
+ const result = configLoader.deepMerge(target, source);
+
+ expect(result).to.deep.equal({ a: 1, b: 3, c: 4 });
+ });
+
+ it('should merge nested objects', () => {
+ const target = {
+ a: 1,
+ b: { x: 1, y: 2 },
+ c: { z: 3 },
+ };
+ const source = {
+ b: { y: 4, w: 5 },
+ c: { z: 6 },
+ };
+
+ const result = configLoader.deepMerge(target, source);
+
+ expect(result).to.deep.equal({
+ a: 1,
+ b: { x: 1, y: 4, w: 5 },
+ c: { z: 6 },
+ });
+ });
+
+ it('should handle arrays by replacing them', () => {
+ const target = {
+ a: [1, 2, 3],
+ b: { items: [4, 5] },
+ };
+ const source = {
+ a: [7, 8],
+ b: { items: [9] },
+ };
+
+ const result = configLoader.deepMerge(target, source);
+
+ expect(result).to.deep.equal({
+ a: [7, 8],
+ b: { items: [9] },
+ });
+ });
+
+ it('should handle null and undefined values', () => {
+ const target = {
+ a: 1,
+ b: null,
+ c: undefined,
+ };
+ const source = {
+ a: null,
+ b: 2,
+ c: 3,
+ };
+
+ const result = configLoader.deepMerge(target, source);
+
+ expect(result).to.deep.equal({
+ a: null,
+ b: 2,
+ c: 3,
+ });
+ });
+
+ it('should handle empty objects', () => {
+ const target = {};
+ const source = { a: 1, b: { c: 2 } };
+
+ const result = configLoader.deepMerge(target, source);
+
+ expect(result).to.deep.equal({ a: 1, b: { c: 2 } });
+ });
+
+ it('should not modify the original objects', () => {
+ const target = { a: 1, b: { c: 2 } };
+ const source = { b: { c: 3 } };
+ const originalTarget = { ...target };
+ const originalSource = { ...source };
+
+ configLoader.deepMerge(target, source);
+
+ expect(target).to.deep.equal(originalTarget);
+ expect(source).to.deep.equal(originalSource);
+ });
+ });
+});
+
+describe('Validation Helpers', () => {
+ describe('isValidGitUrl', () => {
+ it('should validate git URLs correctly', () => {
+ // Valid URLs
+ expect(isValidGitUrl('git://github.com/user/repo.git')).to.be.true;
+ expect(isValidGitUrl('https://github.com/user/repo.git')).to.be.true;
+ expect(isValidGitUrl('ssh://git@github.com/user/repo.git')).to.be.true;
+ expect(isValidGitUrl('user@github.com:user/repo.git')).to.be.true;
+
+ // Invalid URLs
+ expect(isValidGitUrl('not-a-git-url')).to.be.false;
+ expect(isValidGitUrl('http://github.com/user/repo')).to.be.false;
+ expect(isValidGitUrl('')).to.be.false;
+ expect(isValidGitUrl(null)).to.be.false;
+ expect(isValidGitUrl(undefined)).to.be.false;
+ expect(isValidGitUrl(123)).to.be.false;
+ });
+ });
+
+ describe('isValidPath', () => {
+ it('should validate file paths correctly', () => {
+ const cwd = process.cwd();
+
+ // Valid paths
+ expect(isValidPath(path.join(cwd, 'config.json'))).to.be.true;
+ expect(isValidPath(path.join(cwd, 'subfolder/config.json'))).to.be.true;
+ expect(isValidPath('/etc/passwd')).to.be.true;
+ expect(isValidPath('../config.json')).to.be.true;
+
+ // Invalid paths
+ expect(isValidPath('')).to.be.false;
+ expect(isValidPath(null)).to.be.false;
+ expect(isValidPath(undefined)).to.be.false;
+
+ // Additional edge cases
+ expect(isValidPath({})).to.be.false;
+ expect(isValidPath([])).to.be.false;
+ expect(isValidPath(123)).to.be.false;
+ expect(isValidPath(true)).to.be.false;
+ expect(isValidPath('\0invalid')).to.be.false;
+ expect(isValidPath('\u0000')).to.be.false;
+ });
+
+ it('should handle path resolution errors', () => {
+ // Mock path.resolve to throw an error
+ const originalResolve = path.resolve;
+ path.resolve = () => {
+ throw new Error('Mock path resolution error');
+ };
+
+ expect(isValidPath('some/path')).to.be.false;
+
+ // Restore original path.resolve
+ path.resolve = originalResolve;
+ });
+ });
+
+ describe('isValidBranchName', () => {
+ it('should validate git branch names correctly', () => {
+ // Valid branch names
+ expect(isValidBranchName('main')).to.be.true;
+ expect(isValidBranchName('feature/new-feature')).to.be.true;
+ expect(isValidBranchName('release-1.0')).to.be.true;
+ expect(isValidBranchName('fix_123')).to.be.true;
+ expect(isValidBranchName('user/feature/branch')).to.be.true;
+
+ // Invalid branch names
+ expect(isValidBranchName('.invalid')).to.be.false;
+ expect(isValidBranchName('-invalid')).to.be.false;
+ expect(isValidBranchName('branch with spaces')).to.be.false;
+ expect(isValidBranchName('')).to.be.false;
+ expect(isValidBranchName(null)).to.be.false;
+ expect(isValidBranchName(undefined)).to.be.false;
+ expect(isValidBranchName('branch..name')).to.be.false;
+ });
+ });
+});
diff --git a/test/chain.test.js b/test/chain.test.js
index d646b9dc7..1fc749248 100644
--- a/test/chain.test.js
+++ b/test/chain.test.js
@@ -15,65 +15,81 @@ const mockLoader = {
],
};
-const mockPushProcessors = {
- parsePush: sinon.stub(),
- audit: sinon.stub(),
- checkRepoInAuthorisedList: sinon.stub(),
- checkCommitMessages: sinon.stub(),
- checkAuthorEmails: sinon.stub(),
- checkUserPushPermission: sinon.stub(),
- checkIfWaitingAuth: sinon.stub(),
- pullRemote: sinon.stub(),
- writePack: sinon.stub(),
- preReceive: sinon.stub(),
- getDiff: sinon.stub(),
- clearBareClone: sinon.stub(),
- scanDiff: sinon.stub(),
- blockForAuth: sinon.stub(),
+const initMockPushProcessors = (sinon) => {
+ const mockPushProcessors = {
+ parsePush: sinon.stub(),
+ audit: sinon.stub(),
+ checkRepoInAuthorisedList: sinon.stub(),
+ checkCommitMessages: sinon.stub(),
+ checkAuthorEmails: sinon.stub(),
+ checkUserPushPermission: sinon.stub(),
+ checkIfWaitingAuth: sinon.stub(),
+ pullRemote: sinon.stub(),
+ writePack: sinon.stub(),
+ preReceive: sinon.stub(),
+ getDiff: sinon.stub(),
+ gitleaks: sinon.stub(),
+ clearBareClone: sinon.stub(),
+ scanDiff: sinon.stub(),
+ blockForAuth: sinon.stub(),
+ };
+ mockPushProcessors.parsePush.displayName = 'parsePush';
+ mockPushProcessors.audit.displayName = 'audit';
+ mockPushProcessors.checkRepoInAuthorisedList.displayName = 'checkRepoInAuthorisedList';
+ mockPushProcessors.checkCommitMessages.displayName = 'checkCommitMessages';
+ mockPushProcessors.checkAuthorEmails.displayName = 'checkAuthorEmails';
+ mockPushProcessors.checkUserPushPermission.displayName = 'checkUserPushPermission';
+ mockPushProcessors.checkIfWaitingAuth.displayName = 'checkIfWaitingAuth';
+ mockPushProcessors.pullRemote.displayName = 'pullRemote';
+ mockPushProcessors.writePack.displayName = 'writePack';
+ mockPushProcessors.preReceive.displayName = 'preReceive';
+ mockPushProcessors.getDiff.displayName = 'getDiff';
+ mockPushProcessors.gitleaks.displayName = 'gitleaks';
+ mockPushProcessors.clearBareClone.displayName = 'clearBareClone';
+ mockPushProcessors.scanDiff.displayName = 'scanDiff';
+ mockPushProcessors.blockForAuth.displayName = 'blockForAuth';
+ return mockPushProcessors;
};
-mockPushProcessors.parsePush.displayName = 'parsePush';
-mockPushProcessors.audit.displayName = 'audit';
-mockPushProcessors.checkRepoInAuthorisedList.displayName = 'checkRepoInAuthorisedList';
-mockPushProcessors.checkCommitMessages.displayName = 'checkCommitMessages';
-mockPushProcessors.checkAuthorEmails.displayName = 'checkAuthorEmails';
-mockPushProcessors.checkUserPushPermission.displayName = 'checkUserPushPermission';
-mockPushProcessors.checkIfWaitingAuth.displayName = 'checkIfWaitingAuth';
-mockPushProcessors.pullRemote.displayName = 'pullRemote';
-mockPushProcessors.writePack.displayName = 'writePack';
-mockPushProcessors.preReceive.displayName = 'preReceive';
-mockPushProcessors.getDiff.displayName = 'getDiff';
-mockPushProcessors.clearBareClone.displayName = 'clearBareClone';
-mockPushProcessors.scanDiff.displayName = 'scanDiff';
-mockPushProcessors.blockForAuth.displayName = 'blockForAuth';
const mockPreProcessors = {
parseAction: sinon.stub(),
};
+const clearCache = (sandbox) => {
+ delete require.cache[require.resolve('../src/proxy/processors')];
+ delete require.cache[require.resolve('../src/proxy/chain')];
+ sandbox.reset();
+};
+
describe('proxy chain', function () {
let processors;
let chain;
+ let mockPushProcessors;
+ let sandboxSinon;
beforeEach(async () => {
+ // Create a new sandbox for each test
+ sandboxSinon = sinon.createSandbox();
+ // Initialize the mock push processors
+ mockPushProcessors = initMockPushProcessors(sandboxSinon);
+
// Re-import the processors module after clearing the cache
processors = await import('../src/proxy/processors');
// Mock the processors module
- sinon.stub(processors, 'pre').value(mockPreProcessors);
+ sandboxSinon.stub(processors, 'pre').value(mockPreProcessors);
- sinon.stub(processors, 'push').value(mockPushProcessors);
+ sandboxSinon.stub(processors, 'push').value(mockPushProcessors);
// Re-import the chain module after stubbing processors
- chain = (await import('../src/proxy/chain')).default;
+ chain = require('../src/proxy/chain').default;
chain.chainPluginLoader = new PluginLoader([]);
});
afterEach(() => {
// Clear the module from the cache after each test
- delete require.cache[require.resolve('../src/proxy/processors')];
- delete require.cache[require.resolve('../src/proxy/chain')];
- sinon.reset();
+ clearCache(sandboxSinon);
});
it('getChain should set pluginLoaded if loader is undefined', async function () {
@@ -179,6 +195,7 @@ describe('proxy chain', function () {
mockPushProcessors.writePack.resolves(continuingAction);
mockPushProcessors.preReceive.resolves(continuingAction);
mockPushProcessors.getDiff.resolves(continuingAction);
+ mockPushProcessors.gitleaks.resolves(continuingAction);
mockPushProcessors.clearBareClone.resolves(continuingAction);
mockPushProcessors.scanDiff.resolves(continuingAction);
mockPushProcessors.blockForAuth.resolves(continuingAction);
@@ -196,6 +213,7 @@ describe('proxy chain', function () {
expect(mockPushProcessors.writePack.called).to.be.true;
expect(mockPushProcessors.preReceive.called).to.be.true;
expect(mockPushProcessors.getDiff.called).to.be.true;
+ expect(mockPushProcessors.gitleaks.called).to.be.true;
expect(mockPushProcessors.clearBareClone.called).to.be.true;
expect(mockPushProcessors.scanDiff.called).to.be.true;
expect(mockPushProcessors.blockForAuth.called).to.be.true;
@@ -276,6 +294,7 @@ describe('proxy chain', function () {
});
mockPushProcessors.getDiff.resolves(action);
+ mockPushProcessors.gitleaks.resolves(action);
mockPushProcessors.clearBareClone.resolves(action);
mockPushProcessors.scanDiff.resolves(action);
mockPushProcessors.blockForAuth.resolves(action);
@@ -322,6 +341,7 @@ describe('proxy chain', function () {
});
mockPushProcessors.getDiff.resolves(action);
+ mockPushProcessors.gitleaks.resolves(action);
mockPushProcessors.clearBareClone.resolves(action);
mockPushProcessors.scanDiff.resolves(action);
mockPushProcessors.blockForAuth.resolves(action);
@@ -368,6 +388,7 @@ describe('proxy chain', function () {
});
mockPushProcessors.getDiff.resolves(action);
+ mockPushProcessors.gitleaks.resolves(action);
mockPushProcessors.clearBareClone.resolves(action);
mockPushProcessors.scanDiff.resolves(action);
mockPushProcessors.blockForAuth.resolves(action);
@@ -413,6 +434,7 @@ describe('proxy chain', function () {
});
mockPushProcessors.getDiff.resolves(action);
+ mockPushProcessors.gitleaks.resolves(action);
mockPushProcessors.clearBareClone.resolves(action);
mockPushProcessors.scanDiff.resolves(action);
mockPushProcessors.blockForAuth.resolves(action);
diff --git a/test/testConfig.test.js b/test/testConfig.test.js
index 9f5f45419..23b3b1029 100644
--- a/test/testConfig.test.js
+++ b/test/testConfig.test.js
@@ -16,6 +16,7 @@ describe('default configuration', function () {
expect(config.getDatabase()).to.be.eql(defaultSettings.sink[0]);
expect(config.getTempPasswordConfig()).to.be.eql(defaultSettings.tempPassword);
expect(config.getAuthorisedList()).to.be.eql(defaultSettings.authorisedList);
+ expect(config.getRateLimit()).to.be.eql(defaultSettings.rateLimit);
expect(config.getTLSKeyPemPath()).to.be.eql(defaultSettings.tls.key);
expect(config.getTLSCertPemPath()).to.be.eql(defaultSettings.tls.cert);
});
@@ -107,6 +108,21 @@ describe('user configuration', function () {
expect(config.getTLSCertPemPath()).to.be.eql(user.tls.cert);
});
+ it('should override default settings for rate limiting', function () {
+ const limitConfig = {
+ rateLimit: {
+ windowMs: 60000,
+ limit: 1500,
+ },
+ };
+ fs.writeFileSync(tempUserFile, JSON.stringify(limitConfig));
+
+ const config = require('../src/config');
+
+ expect(config.getRateLimit().windowMs).to.be.eql(limitConfig.rateLimit.windowMs);
+ expect(config.getRateLimit().limit).to.be.eql(limitConfig.rateLimit.limit);
+ });
+
afterEach(function () {
fs.rmSync(tempUserFile);
fs.rmdirSync(tempDir);
diff --git a/tsconfig.json b/tsconfig.json
index 805153d01..a389ca8c7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,6 +8,7 @@
"moduleResolution": "Node",
"strict": true,
"noEmit": true,
+ "declaration": true,
"skipLibCheck": true,
"isolatedModules": true,
"module": "CommonJS",
@@ -15,5 +16,6 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
- "include": ["src"]
+ "include": ["."],
+ "exclude": ["experimental/**", "plugins/**"]
}
diff --git a/tsconfig.publish.json b/tsconfig.publish.json
new file mode 100644
index 000000000..ef9be14f7
--- /dev/null
+++ b/tsconfig.publish.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "outDir": "./dist"
+ },
+ "exclude": [
+ "experimental/**",
+ "plugins/**",
+ "./dist/**",
+ "./src/ui",
+ "./src/**/*.jsx",
+ "./src/context.js"
+ ]
+}
diff --git a/website/docs/configuration/overview.mdx b/website/docs/configuration/overview.mdx
index 5493d54f6..274de5443 100644
--- a/website/docs/configuration/overview.mdx
+++ b/website/docs/configuration/overview.mdx
@@ -7,6 +7,7 @@ description: How to customise push protections and policies
On installation, GitProxy ships with an [out-of-the-box configuration](https://github.com/finos/git-proxy/blob/main/proxy.config.json). This is fine for
demonstration purposes but is likely not what you want to deploy into your environment.
+
### Customise configuration
To customise your GitProxy configuration, create a `proxy.config.json` in your current
@@ -44,8 +45,9 @@ npx -- @finos/git-proxy --config ./config.json
```
### Set ports with ENV variables
+
By default, GitProxy uses port 8000 to expose the Git Server and 8080 for the frontend application.
-The ports can be changed by setting the `GIT_PROXY_SERVER_PORT`, `GIT_PROXY_HTTPS_SERVER_PORT` (optional) and `GIT_PROXY_UI_PORT`
+The ports can be changed by setting the `GIT_PROXY_SERVER_PORT`, `GIT_PROXY_HTTPS_SERVER_PORT` (optional) and `GIT_PROXY_UI_PORT`
environment variables:
```
@@ -54,10 +56,10 @@ export GIT_PROXY_SERVER_PORT="9090"
export GIT_PROXY_HTTPS_SERVER_PORT="9443"
```
-Note that `GIT_PROXY_UI_PORT` is needed for both server and UI Node processes,
+Note that `GIT_PROXY_UI_PORT` is needed for both server and UI Node processes,
whereas `GIT_PROXY_SERVER_PORT` (and `GIT_PROXY_HTTPS_SERVER_PORT`) is only needed by the server process.
-By default, GitProxy CLI connects to GitProxy running on localhost and default port. This can be
+By default, GitProxy CLI connects to GitProxy running on localhost and default port. This can be
changed by setting the `GIT_PROXY_UI_HOST` and `GIT_PROXY_UI_PORT` environment variables:
```
@@ -79,5 +81,66 @@ To validate your configuration at a custom file location, run:
git-proxy --validate --config ./config.json
```
+### Configuration Sources
+
+GitProxy supports dynamic configuration loading from multiple sources. This feature allows you to manage your configuration from external sources and update it without restarting the service. Configuration sources can be files, HTTP endpoints, or Git repositories.
+
+To enable configuration sources, add the `configurationSources` section to your configuration:
+
+```json
+{
+ "configurationSources": {
+ "enabled": true,
+ "reloadIntervalSeconds": 60,
+ "merge": false,
+ "sources": [
+ {
+ "type": "file",
+ "enabled": true,
+ "path": "./external-config.json"
+ },
+ {
+ "type": "http",
+ "enabled": true,
+ "url": "http://config-service/git-proxy-config",
+ "headers": {},
+ "auth": {
+ "type": "bearer",
+ "token": "your-token"
+ }
+ },
+ {
+ "type": "git",
+ "enabled": true,
+ "repository": "https://git-server.com/project/git-proxy-config",
+ "branch": "main",
+ "path": "git-proxy/config.json",
+ "auth": {
+ "type": "ssh",
+ "privateKeyPath": "/path/to/.ssh/id_rsa"
+ }
+ }
+ ]
+ }
+}
+```
+
+The configuration options for `configurationSources` are:
+
+- `enabled`: Enable/disable dynamic configuration loading
+- `reloadIntervalSeconds`: How often to check for configuration updates (in seconds)
+- `merge`: When true, merges configurations from all enabled sources. When false, uses the last successful configuration load. This can be used to upload only partial configuration to external source
+- `sources`: Array of configuration sources to load from
+
+Each source can be one of three types:
+
+1. `file`: Load from a local JSON file
+2. `http`: Load from an HTTP endpoint
+3. `git`: Load from a Git repository
+When configuration changes are detected, GitProxy will:
+1. Validate the new configuration
+2. Stop existing services
+3. Apply the new configuration
+4. Restart services with the updated configuration
diff --git a/website/docs/configuration/reference.mdx b/website/docs/configuration/reference.mdx
index 6a7eceedf..3b8402305 100644
--- a/website/docs/configuration/reference.mdx
+++ b/website/docs/configuration/reference.mdx
@@ -7,11 +7,11 @@ description: JSON schema reference documentation for GitProxy
**Title:** GitProxy configuration file
-| | |
-| ------------------------- | ------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Not allowed]](# "Additional Properties not allowed.") |
+| | |
+| ------------------------- | ----------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Not allowed |
**Description:** Configuration for customizing git-proxy
@@ -63,11 +63,11 @@ description: JSON schema reference documentation for GitProxy
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
**Description:** Third party APIs
@@ -80,11 +80,11 @@ description: JSON schema reference documentation for GitProxy
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
**Description:** Enforce rules and patterns on commits including e-mail and message
@@ -97,11 +97,11 @@ description: JSON schema reference documentation for GitProxy
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
**Description:** Customisable questions to add to attestation form
@@ -114,11 +114,11 @@ description: JSON schema reference documentation for GitProxy
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
**Description:** Provide domains to use alternative to the defaults
@@ -127,7 +127,88 @@ description: JSON schema reference documentation for GitProxy
- 8. [Optional] Property GitProxy configuration file > privateOrganizations
+ 8. [Optional] Property GitProxy configuration file > rateLimit
+
+
+
+| | |
+| ------------------------- | ----------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Not allowed |
+
+**Description:** API Rate limiting configuration.
+
+
+
+ 8.1. [Required] Property GitProxy configuration file > rateLimit > windowMs
+
+
+
+| | |
+| ------------ | -------- |
+| **Type** | `number` |
+| **Required** | Yes |
+
+**Description:** How long to remember requests for, in milliseconds (default 10 mins).
+
+
+
+
+
+
+ 8.2. [Required] Property GitProxy configuration file > rateLimit > limit
+
+
+
+| | |
+| ------------ | -------- |
+| **Type** | `number` |
+| **Required** | Yes |
+
+**Description:** How many requests to allow (default 150).
+
+
+
+
+
+
+ 8.3. [Optional] Property GitProxy configuration file > rateLimit > statusCode
+
+
+
+| | |
+| ------------ | -------- |
+| **Type** | `number` |
+| **Required** | No |
+
+**Description:** HTTP status code after limit is reached (default is 429).
+
+
+
+
+
+
+ 8.4. [Optional] Property GitProxy configuration file > rateLimit > message
+
+
+
+| | |
+| ------------ | -------- |
+| **Type** | `string` |
+| **Required** | No |
+
+**Description:** Response to return after limit is reached.
+
+
+
+
+
+
+
+
+
+ 9. [Optional] Property GitProxy configuration file > privateOrganizations
@@ -143,7 +224,7 @@ description: JSON schema reference documentation for GitProxy
- 9. [Optional] Property GitProxy configuration file > urlShortener
+ 10. [Optional] Property GitProxy configuration file > urlShortener
@@ -159,7 +240,7 @@ description: JSON schema reference documentation for GitProxy
- 10. [Optional] Property GitProxy configuration file > contactEmail
+ 11. [Optional] Property GitProxy configuration file > contactEmail
@@ -175,7 +256,7 @@ description: JSON schema reference documentation for GitProxy
- 11. [Optional] Property GitProxy configuration file > csrfProtection
+ 12. [Optional] Property GitProxy configuration file > csrfProtection
@@ -191,7 +272,7 @@ description: JSON schema reference documentation for GitProxy
- 12. [Optional] Property GitProxy configuration file > plugins
+ 13. [Optional] Property GitProxy configuration file > plugins
@@ -206,7 +287,7 @@ description: JSON schema reference documentation for GitProxy
| ------------------------------- | ----------- |
| [plugins items](#plugins_items) | - |
-### 12.1. GitProxy configuration file > plugins > plugins items
+### 13.1. GitProxy configuration file > plugins > plugins items
| | |
| ------------ | -------- |
@@ -218,7 +299,7 @@ description: JSON schema reference documentation for GitProxy
- 13. [Optional] Property GitProxy configuration file > authorisedList
+ 14. [Optional] Property GitProxy configuration file > authorisedList
@@ -233,18 +314,18 @@ description: JSON schema reference documentation for GitProxy
| --------------------------------------- | ----------- |
| [authorisedRepo](#authorisedList_items) | - |
-### 13.1. GitProxy configuration file > authorisedList > authorisedRepo
+### 14.1. GitProxy configuration file > authorisedList > authorisedRepo
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
-| **Defined in** | #/definitions/authorisedRepo |
+| | |
+| ------------------------- | ---------------------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
+| **Defined in** | #/definitions/authorisedRepo |
- 13.1.1. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > project
+ 14.1.1. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > project
@@ -258,7 +339,7 @@ description: JSON schema reference documentation for GitProxy
- 13.1.2. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > name
+ 14.1.2. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > name
@@ -272,7 +353,7 @@ description: JSON schema reference documentation for GitProxy
- 13.1.3. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > url
+ 14.1.3. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > url
@@ -289,7 +370,7 @@ description: JSON schema reference documentation for GitProxy
- 14. [Optional] Property GitProxy configuration file > sink
+ 15. [Optional] Property GitProxy configuration file > sink
@@ -304,18 +385,18 @@ description: JSON schema reference documentation for GitProxy
| ------------------------------- | ----------- |
| [database](#sink_items) | - |
-### 14.1. GitProxy configuration file > sink > database
+### 15.1. GitProxy configuration file > sink > database
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
-| **Defined in** | #/definitions/database |
+| | |
+| ------------------------- | ---------------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
+| **Defined in** | #/definitions/database |
- 14.1.1. [Required] Property GitProxy configuration file > sink > sink items > type
+ 15.1.1. [Required] Property GitProxy configuration file > sink > sink items > type
@@ -329,7 +410,7 @@ description: JSON schema reference documentation for GitProxy
- 14.1.2. [Required] Property GitProxy configuration file > sink > sink items > enabled
+ 15.1.2. [Required] Property GitProxy configuration file > sink > sink items > enabled
@@ -343,7 +424,7 @@ description: JSON schema reference documentation for GitProxy
- 14.1.3. [Optional] Property GitProxy configuration file > sink > sink items > connectionString
+ 15.1.3. [Optional] Property GitProxy configuration file > sink > sink items > connectionString
@@ -357,30 +438,30 @@ description: JSON schema reference documentation for GitProxy
- 14.1.4. [Optional] Property GitProxy configuration file > sink > sink items > options
+ 15.1.4. [Optional] Property GitProxy configuration file > sink > sink items > options
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
- 14.1.5. [Optional] Property GitProxy configuration file > sink > sink items > params
+ 15.1.5. [Optional] Property GitProxy configuration file > sink > sink items > params
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
@@ -390,7 +471,7 @@ description: JSON schema reference documentation for GitProxy
- 15. [Optional] Property GitProxy configuration file > authentication
+ 16. [Optional] Property GitProxy configuration file > authentication
@@ -405,18 +486,18 @@ description: JSON schema reference documentation for GitProxy
| --------------------------------------- | ----------- |
| [authentication](#authentication_items) | - |
-### 15.1. GitProxy configuration file > authentication > authentication
+### 16.1. GitProxy configuration file > authentication > authentication
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
-| **Defined in** | #/definitions/authentication |
+| | |
+| ------------------------- | ---------------------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
+| **Defined in** | #/definitions/authentication |
- 15.1.1. [Required] Property GitProxy configuration file > authentication > authentication items > type
+ 16.1.1. [Required] Property GitProxy configuration file > authentication > authentication items > type
@@ -430,7 +511,7 @@ description: JSON schema reference documentation for GitProxy
- 15.1.2. [Required] Property GitProxy configuration file > authentication > authentication items > enabled
+ 16.1.2. [Required] Property GitProxy configuration file > authentication > authentication items > enabled
@@ -444,15 +525,15 @@ description: JSON schema reference documentation for GitProxy
- 15.1.3. [Optional] Property GitProxy configuration file > authentication > authentication items > options
+ 16.1.3. [Optional] Property GitProxy configuration file > authentication > authentication items > options
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
@@ -462,21 +543,21 @@ description: JSON schema reference documentation for GitProxy
- 16. [Optional] Property GitProxy configuration file > tempPassword
+ 17. [Optional] Property GitProxy configuration file > tempPassword
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
**Description:** Toggle the generation of temporary password for git-proxy admin user
- 16.1. [Optional] Property GitProxy configuration file > tempPassword > sendEmail
+ 17.1. [Optional] Property GitProxy configuration file > tempPassword > sendEmail
@@ -490,15 +571,15 @@ description: JSON schema reference documentation for GitProxy
- 16.2. [Optional] Property GitProxy configuration file > tempPassword > emailConfig
+ 17.2. [Optional] Property GitProxy configuration file > tempPassword > emailConfig
-| | |
-| ------------------------- | ------------------------------------------------------------------------- |
-| **Type** | `object` |
-| **Required** | No |
-| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") |
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
**Description:** Generic object to configure nodemailer. For full type information, please see https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nodemailer
@@ -508,5 +589,164 @@ description: JSON schema reference documentation for GitProxy
-----------------------------------------------------------------------------------------------------------------------------
-Generated using [json-schema-for-humans](https://github.com/coveooss/json-schema-for-humans) on 2024-10-22 at 16:45:32 +0100
+
+
+ 18. [Optional] Property GitProxy configuration file > tls
+
+
+
+| | |
+| ------------------------- | ---------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | Any type allowed |
+
+**Description:** TLS configuration for secure connections
+
+
+
+ 18.1. [Required] Property GitProxy configuration file > tls > enabled
+
+
+
+| | |
+| ------------ | --------- |
+| **Type** | `boolean` |
+| **Required** | Yes |
+
+
+
+
+
+
+ 18.2. [Required] Property GitProxy configuration file > tls > key
+
+
+
+| | |
+| ------------ | -------- |
+| **Type** | `string` |
+| **Required** | Yes |
+
+
+
+
+
+
+ 18.3. [Required] Property GitProxy configuration file > tls > cert
+
+
+
+| | |
+| ------------ | -------- |
+| **Type** | `string` |
+| **Required** | Yes |
+
+
+
+
+
+
+
+
+
+ 19. [Optional] Property GitProxy configuration file > configurationSources
+
+
+
+| | |
+| ------------------------- | ------------------------------------------------------- |
+| **Type** | `object` |
+| **Required** | No |
+| **Additional properties** | [[Not allowed]](# "Additional Properties not allowed.") |
+
+**Description:** Configuration for dynamic loading from external sources
+
+
+
+ 19.1. [Optional] Property configurationSources > enabled
+
+
+
+| | |
+| ------------ | --------- |
+| **Type** | `boolean` |
+| **Required** | No |
+
+**Description:** Enable/disable dynamic configuration loading
+
+
+
+
+
+
+ 19.2. [Optional] Property configurationSources > reloadIntervalSeconds
+
+
+
+| | |
+| ------------ | -------- |
+| **Type** | `number` |
+| **Required** | No |
+
+**Description:** How often to check for configuration updates (in seconds)
+
+
+
+
+
+
+ 19.3. [Optional] Property configurationSources > merge
+
+
+
+| | |
+| ------------ | --------- |
+| **Type** | `boolean` |
+| **Required** | No |
+
+**Description:** When true, merges configurations from all enabled sources. When false, uses the last successful configuration load
+
+
+
+
+
+
+ 19.4. [Optional] Property configurationSources > sources
+
+
+
+| | |
+| ------------ | ------- |
+| **Type** | `array` |
+| **Required** | No |
+
+**Description:** Array of configuration sources to load from
+
+Each item in the array must be an object with the following properties:
+
+- `type`: (Required) Type of configuration source (`"file"`, `"http"`, or `"git"`)
+- `enabled`: (Required) Whether this source is enabled
+- `path`: (Required for `file` type) Path to the configuration file
+- `url`: (Required for `http` type) URL of the configuration endpoint
+- `repository`: (Required for `git` type) Git repository URL
+- `branch`: (Optional for `git` type) Branch to use
+- `path`: (Required for `git` type) Path to configuration file in repository
+- `headers`: (Optional for `http` type) HTTP headers to include
+- `auth`: (Optional) Authentication configuration
+ - For `http` type:
+ - `type`: `"bearer"`
+ - `token`: Bearer token value
+ - For `git` type:
+ - `type`: `"ssh"`
+ - `privateKeyPath`: Path to SSH private key
+
+
+
+
+
+
+
+---
+
+Generated using [json-schema-for-humans](https://github.com/coveooss/json-schema-for-humans) on 2025-05-01 at 18:17:32 +0100
diff --git a/website/package.json b/website/package.json
index 446392c87..9456a83c4 100644
--- a/website/package.json
+++ b/website/package.json
@@ -13,13 +13,13 @@
"@docusaurus/core": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/plugin-google-gtag": "^3.7.0",
- "axios": "^1.8.4",
+ "axios": "^1.9.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
- "eslint": "^9.23.0",
- "eslint-plugin-react": "^7.37.4",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
+ "eslint": "^9.27.0",
+ "eslint-plugin-react": "^7.37.5",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
"react-player": "^2.16.0",
"react-slick": "^0.30.3",
"react-social-media-embed": "^2.5.18",
diff --git a/website/yarn.lock b/website/yarn.lock
index 7cae312cd..ce259a41f 100644
--- a/website/yarn.lock
+++ b/website/yarn.lock
@@ -2832,24 +2832,24 @@
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
-"@eslint/config-array@^0.19.2":
- version "0.19.2"
- resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa"
- integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==
+"@eslint/config-array@^0.20.0":
+ version "0.20.0"
+ resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.20.0.tgz#7a1232e82376712d3340012a2f561a2764d1988f"
+ integrity sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==
dependencies:
"@eslint/object-schema" "^2.1.6"
debug "^4.3.1"
minimatch "^3.1.2"
-"@eslint/config-helpers@^0.2.0":
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.0.tgz#12dc8d65c31c4b6c3ebf0758db6601eb7692ce59"
- integrity sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==
+"@eslint/config-helpers@^0.2.1":
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz#26042c028d1beee5ce2235a7929b91c52651646d"
+ integrity sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==
-"@eslint/core@^0.12.0":
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.12.0.tgz#5f960c3d57728be9f6c65bd84aa6aa613078798e"
- integrity sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==
+"@eslint/core@^0.14.0":
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.14.0.tgz#326289380968eaf7e96f364e1e4cf8f3adf2d003"
+ integrity sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==
dependencies:
"@types/json-schema" "^7.0.15"
@@ -2868,22 +2868,22 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@9.23.0":
- version "9.23.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.23.0.tgz#c09ded4f3dc63b40b933bcaeb853fceddb64da30"
- integrity sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==
+"@eslint/js@9.27.0":
+ version "9.27.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.27.0.tgz#181a23460877c484f6dd03890f4e3fa2fdeb8ff0"
+ integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
-"@eslint/plugin-kit@^0.2.7":
- version "0.2.7"
- resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz#9901d52c136fb8f375906a73dcc382646c3b6a27"
- integrity sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==
+"@eslint/plugin-kit@^0.3.1":
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz#b71b037b2d4d68396df04a8c35a49481e5593067"
+ integrity sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==
dependencies:
- "@eslint/core" "^0.12.0"
+ "@eslint/core" "^0.14.0"
levn "^0.4.1"
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
@@ -4132,10 +4132,10 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
-axios@^1.8.4:
- version "1.8.4"
- resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447"
- integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==
+axios@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901"
+ integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
@@ -4340,6 +4340,14 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1:
es-errors "^1.3.0"
function-bind "^1.1.2"
+call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
call-bind@^1.0.5, call-bind@^1.0.7, call-bind@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
@@ -4358,6 +4366,14 @@ call-bound@^1.0.2, call-bound@^1.0.3:
call-bind-apply-helpers "^1.0.1"
get-intrinsic "^1.2.6"
+call-bound@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+ integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ get-intrinsic "^1.3.0"
+
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -5469,7 +5485,7 @@ es-module-lexer@^1.2.1:
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78"
integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==
-es-object-atoms@^1.0.0:
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
@@ -5537,10 +5553,10 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
-eslint-plugin-react@^7.37.4:
- version "7.37.4"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz#1b6c80b6175b6ae4b26055ae4d55d04c414c7181"
- integrity sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==
+eslint-plugin-react@^7.37.5:
+ version "7.37.5"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065"
+ integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==
dependencies:
array-includes "^3.1.8"
array.prototype.findlast "^1.2.5"
@@ -5552,7 +5568,7 @@ eslint-plugin-react@^7.37.4:
hasown "^2.0.2"
jsx-ast-utils "^2.4.1 || ^3.0.0"
minimatch "^3.1.2"
- object.entries "^1.1.8"
+ object.entries "^1.1.9"
object.fromentries "^2.0.8"
object.values "^1.2.1"
prop-types "^15.8.1"
@@ -5587,19 +5603,19 @@ eslint-visitor-keys@^4.2.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
-eslint@^9.23.0:
- version "9.23.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.23.0.tgz#b88f3ab6dc83bcb927fdb54407c69ffe5f2441a6"
- integrity sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==
+eslint@^9.27.0:
+ version "9.27.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.27.0.tgz#a587d3cd5b844b68df7898944323a702afe38979"
+ integrity sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.12.1"
- "@eslint/config-array" "^0.19.2"
- "@eslint/config-helpers" "^0.2.0"
- "@eslint/core" "^0.12.0"
+ "@eslint/config-array" "^0.20.0"
+ "@eslint/config-helpers" "^0.2.1"
+ "@eslint/core" "^0.14.0"
"@eslint/eslintrc" "^3.3.1"
- "@eslint/js" "9.23.0"
- "@eslint/plugin-kit" "^0.2.7"
+ "@eslint/js" "9.27.0"
+ "@eslint/plugin-kit" "^0.3.1"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@humanwhocodes/retry" "^0.4.2"
@@ -6118,6 +6134,22 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@
hasown "^2.0.2"
math-intrinsics "^1.1.0"
+get-intrinsic@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+ 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"
+
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
@@ -8375,14 +8407,15 @@ object.assign@^4.1.4, object.assign@^4.1.7:
has-symbols "^1.1.0"
object-keys "^1.1.1"
-object.entries@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41"
- integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==
+object.entries@^1.1.9:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3"
+ integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==
dependencies:
- call-bind "^1.0.7"
+ call-bind "^1.0.8"
+ call-bound "^1.0.4"
define-properties "^1.2.1"
- es-object-atoms "^1.0.0"
+ es-object-atoms "^1.1.1"
object.fromentries@^2.0.8:
version "2.0.8"
@@ -9454,12 +9487,12 @@ react-dev-utils@^12.0.1:
strip-ansi "^6.0.1"
text-table "^0.2.0"
-react-dom@^19.0.0:
- version "19.0.0"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57"
- integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
+react-dom@^19.1.0:
+ version "19.1.0"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623"
+ integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
dependencies:
- scheduler "^0.25.0"
+ scheduler "^0.26.0"
react-error-overlay@^6.0.11:
version "6.0.11"
@@ -9601,10 +9634,10 @@ react-youtube@^10.1.0:
prop-types "15.8.1"
youtube-player "5.5.2"
-react@^19.0.0:
- version "19.0.0"
- resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd"
- integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
+react@^19.1.0:
+ version "19.1.0"
+ resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
+ integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
readable-stream@^2.0.1:
version "2.3.8"
@@ -10021,10 +10054,10 @@ sax@^1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==
-scheduler@^0.25.0:
- version "0.25.0"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015"
- integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
+scheduler@^0.26.0:
+ version "0.26.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
+ integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
schema-utils@2.7.0:
version "2.7.0"