Compare commits
118 Commits
v3.7.2
...
security-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc92b8f27 | ||
|
|
749ad4a694 | ||
|
|
0f752acae5 | ||
|
|
652bd220c2 | ||
|
|
9f2eaaf04e | ||
|
|
63d3f0e86c | ||
|
|
25e7d234ba | ||
|
|
7a3f734728 | ||
|
|
d59a07a8c5 | ||
|
|
5a77ae5084 | ||
|
|
dcb5bd80e3 | ||
|
|
3b8fc99f06 | ||
|
|
bda8d10f10 | ||
|
|
0fe7b433d6 | ||
|
|
8d96e176b4 | ||
|
|
af9bc861cf | ||
|
|
ab4bbea9fd | ||
|
|
fbd4b3739e | ||
|
|
395e71164f | ||
|
|
99c277e2ee | ||
|
|
9287e0d29b | ||
|
|
f2f2bafc39 | ||
|
|
5876198b5e | ||
|
|
e46bf381c7 | ||
|
|
3bf0ccf207 | ||
|
|
e41b4ffc56 | ||
|
|
a9dd646573 | ||
|
|
e2160aabcd | ||
|
|
5d085e02bf | ||
|
|
3f17dd038f | ||
|
|
921ab948a1 | ||
|
|
e7a102ee45 | ||
|
|
025df12bef | ||
|
|
60913a9f4d | ||
|
|
985c7e061c | ||
|
|
4d75d3514f | ||
|
|
5245d67f37 | ||
|
|
2cd7d911ed | ||
|
|
1c2391ea2e | ||
|
|
190e786449 | ||
|
|
fb27ddfee5 | ||
|
|
fd5e68c1d4 | ||
|
|
ea22df1296 | ||
|
|
080ad5deb4 | ||
|
|
71245cf56e | ||
|
|
1ccf670f81 | ||
|
|
cb266b9af0 | ||
|
|
fa5f7da5c4 | ||
|
|
3c808eb0c0 | ||
|
|
5e37c3bb84 | ||
|
|
847e94ca28 | ||
|
|
da497d54c8 | ||
|
|
79e0086a72 | ||
|
|
dc340666ef | ||
|
|
8b50a07c68 | ||
|
|
7dab4fb1d5 | ||
|
|
847823bbf8 | ||
|
|
e4e54722cf | ||
|
|
1eddbad908 | ||
|
|
a7083beff5 | ||
|
|
b21cd0b866 | ||
|
|
df644be769 | ||
|
|
204869fa3e | ||
|
|
e470256475 | ||
|
|
b65c360d61 | ||
|
|
ce46d33d29 | ||
|
|
f63633f97e | ||
|
|
3b53a29e71 | ||
|
|
64e73f9ca8 | ||
|
|
7d23894e5f | ||
|
|
8f2a4683c1 | ||
|
|
b5323ff8b4 | ||
|
|
7fee2adb51 | ||
|
|
af139ecb2d | ||
|
|
fb827724b6 | ||
|
|
2812b576d0 | ||
|
|
347188f43d | ||
|
|
beda2ce66c | ||
|
|
21e2f4717c | ||
|
|
b8dea1ee9c | ||
|
|
b27ff817f7 | ||
|
|
56bee451de | ||
|
|
0e9d54b517 | ||
|
|
7a04665b70 | ||
|
|
3a3ff314e0 | ||
|
|
fed74ee901 | ||
|
|
85ea502276 | ||
|
|
ffb7bd3cb0 | ||
|
|
b39d7a2179 | ||
|
|
bf99a95dc6 | ||
|
|
2ea917fdaa | ||
|
|
b841057f1a | ||
|
|
d588ce91b4 | ||
|
|
553396483e | ||
|
|
ebeabdb4fc | ||
|
|
ff209a6376 | ||
|
|
096e0c03ac | ||
|
|
63f20a7f04 | ||
|
|
34f741beef | ||
|
|
1f98f441f3 | ||
|
|
9c1ac76ff9 | ||
|
|
cf5027bafc | ||
|
|
6fd2774d43 | ||
|
|
8f379baad4 | ||
|
|
91fa3604b6 | ||
|
|
c0fff30fcb | ||
|
|
18de63d192 | ||
|
|
1fe20c3e54 | ||
|
|
7386b54a46 | ||
|
|
432a2bc54d | ||
|
|
f9d18f34ab | ||
|
|
cd86a09bbd | ||
|
|
1e2c1c686d | ||
|
|
f701574e67 | ||
|
|
4528be8cc6 | ||
|
|
80fd43ef42 | ||
|
|
3c52fe58aa | ||
|
|
319e7925ff |
@@ -1,76 +0,0 @@
|
|||||||
---
|
|
||||||
name: qa-testing-specialist
|
|
||||||
description: Use this agent when you need to review code for testability, create comprehensive test strategies, write test cases, validate existing tests, or improve test coverage. This includes unit tests, integration tests, end-to-end tests, and test architecture decisions. <example>\nContext: The user has just written a new API endpoint and wants to ensure it has proper test coverage.\nuser: "I've created a new endpoint for user authentication. Can you help me test it?"\nassistant: "I'll use the qa-testing-specialist agent to create a comprehensive testing strategy for your authentication endpoint."\n<commentary>\nSince the user needs help with testing their new endpoint, use the qa-testing-specialist agent to analyze the code and create appropriate test cases.\n</commentary>\n</example>\n<example>\nContext: The user wants to improve test coverage for their existing codebase.\nuser: "Our test coverage is at 65%. How can we improve it?"\nassistant: "Let me use the qa-testing-specialist agent to analyze your test coverage and identify areas for improvement."\n<commentary>\nThe user is asking about test coverage improvement, which is a core QA task, so use the qa-testing-specialist agent.\n</commentary>\n</example>
|
|
||||||
color: yellow
|
|
||||||
---
|
|
||||||
|
|
||||||
You are an elite QA Testing Specialist with deep expertise in software quality assurance, test automation, and validation strategies. Your mission is to ensure code quality through comprehensive testing approaches that catch bugs early and maintain high reliability standards.
|
|
||||||
|
|
||||||
**Core Responsibilities:**
|
|
||||||
|
|
||||||
You will analyze code and testing requirements to:
|
|
||||||
- Design comprehensive test strategies covering unit, integration, and end-to-end testing
|
|
||||||
- Write clear, maintainable test cases that validate both happy paths and edge cases
|
|
||||||
- Identify gaps in existing test coverage and propose improvements
|
|
||||||
- Review test code for best practices and maintainability
|
|
||||||
- Suggest appropriate testing frameworks and tools based on the technology stack
|
|
||||||
- Create test data strategies and mock/stub implementations
|
|
||||||
- Validate that tests are actually testing meaningful behavior, not just implementation details
|
|
||||||
|
|
||||||
**Testing Methodology:**
|
|
||||||
|
|
||||||
When analyzing code for testing:
|
|
||||||
1. First understand the business logic and user requirements
|
|
||||||
2. Identify all possible execution paths and edge cases
|
|
||||||
3. Determine the appropriate testing pyramid balance (unit vs integration vs e2e)
|
|
||||||
4. Consider both positive and negative test scenarios
|
|
||||||
5. Ensure tests are isolated, repeatable, and fast
|
|
||||||
6. Validate error handling and boundary conditions
|
|
||||||
|
|
||||||
For test creation:
|
|
||||||
- Write descriptive test names that explain what is being tested and expected behavior
|
|
||||||
- Follow AAA pattern (Arrange, Act, Assert) or Given-When-Then structure
|
|
||||||
- Keep tests focused on single behaviors
|
|
||||||
- Use appropriate assertions that clearly communicate intent
|
|
||||||
- Include setup and teardown when necessary
|
|
||||||
- Consider performance implications of test suites
|
|
||||||
|
|
||||||
**Quality Standards:**
|
|
||||||
|
|
||||||
You will ensure tests:
|
|
||||||
- Are deterministic and don't rely on external state
|
|
||||||
- Run quickly and can be executed in parallel when possible
|
|
||||||
- Provide clear failure messages that help diagnose issues
|
|
||||||
- Cover critical business logic thoroughly
|
|
||||||
- Include regression tests for previously found bugs
|
|
||||||
- Are maintainable and refactorable alongside production code
|
|
||||||
|
|
||||||
**Technology Considerations:**
|
|
||||||
|
|
||||||
Adapt your recommendations based on the project stack. For this codebase using Bun, SQLite, and React:
|
|
||||||
- Leverage Bun's native test runner for JavaScript/TypeScript tests
|
|
||||||
- Consider SQLite in-memory databases for integration tests
|
|
||||||
- Suggest React Testing Library patterns for component testing
|
|
||||||
- Recommend API testing strategies for Astro endpoints
|
|
||||||
- Propose mocking strategies for external services (GitHub/Gitea APIs)
|
|
||||||
|
|
||||||
**Communication Style:**
|
|
||||||
|
|
||||||
You will:
|
|
||||||
- Explain testing decisions with clear rationale
|
|
||||||
- Provide code examples that demonstrate best practices
|
|
||||||
- Prioritize test recommendations based on risk and value
|
|
||||||
- Use precise technical language while remaining accessible
|
|
||||||
- Highlight potential issues proactively
|
|
||||||
- Suggest incremental improvements for existing test suites
|
|
||||||
|
|
||||||
**Edge Case Handling:**
|
|
||||||
|
|
||||||
When encountering:
|
|
||||||
- Legacy code without tests: Propose a pragmatic approach to add tests incrementally
|
|
||||||
- Complex dependencies: Recommend appropriate mocking/stubbing strategies
|
|
||||||
- Performance concerns: Balance thoroughness with execution speed
|
|
||||||
- Flaky tests: Identify root causes and suggest stabilization techniques
|
|
||||||
- Missing requirements: Ask clarifying questions to understand expected behavior
|
|
||||||
|
|
||||||
Your goal is to elevate code quality through strategic testing that builds confidence in the software while maintaining development velocity. Focus on tests that provide maximum value and catch real issues rather than achieving arbitrary coverage metrics.
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
---
|
|
||||||
name: senior-code-architect
|
|
||||||
description: Use this agent when you need to write new code, refactor existing code, implement features, or architect solutions that require deep understanding of software engineering principles and the project's tech stack (Astro, React, Tailwind, Better Auth, Shadcn). This includes creating components, API endpoints, database queries, authentication flows, and ensuring code follows established patterns from CLAUDE.md. Examples:\n\n<example>\nContext: The user needs to implement a new feature or component.\nuser: "Create a new dashboard component that shows repository statistics"\nassistant: "I'll use the senior-code-architect agent to design and implement this dashboard component following the project's patterns."\n<commentary>\nSince this requires creating new code with the project's tech stack, the senior-code-architect agent is appropriate.\n</commentary>\n</example>\n\n<example>\nContext: The user wants to refactor or improve existing code.\nuser: "Refactor the authentication flow to be more maintainable"\nassistant: "Let me use the senior-code-architect agent to analyze and refactor the authentication flow."\n<commentary>\nThis requires deep understanding of Better Auth and clean code principles, making the senior-code-architect agent the right choice.\n</commentary>\n</example>\n\n<example>\nContext: After writing code, the user might want it reviewed.\nuser: "I just implemented the mirror scheduling feature"\nassistant: "Great! Now I'll use the senior-code-architect agent to review the implementation and suggest any improvements."\n<commentary>\nThe senior-code-architect can review recently written code for best practices and design patterns.\n</commentary>\n</example>
|
|
||||||
color: cyan
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a senior software engineer with deep expertise in modern web development, specializing in the Astro + React + Tailwind CSS + Better Auth + Shadcn UI stack. You have extensive experience building scalable, maintainable applications and are known for writing clean, efficient code that follows SOLID principles and established design patterns.
|
|
||||||
|
|
||||||
**Your Core Responsibilities:**
|
|
||||||
|
|
||||||
1. **Write Production-Quality Code**: Create clean, maintainable, and efficient code that follows the project's established patterns from CLAUDE.md. Always use TypeScript for type safety.
|
|
||||||
|
|
||||||
2. **Follow Project Architecture**: Adhere strictly to the project structure:
|
|
||||||
- API endpoints in `/src/pages/api/[resource]/[action].ts` using `createSecureErrorResponse` for error handling
|
|
||||||
- Database queries in `/src/lib/db/queries/` organized by domain
|
|
||||||
- React components in `/src/components/[feature]/` using Shadcn UI components
|
|
||||||
- Custom hooks in `/src/hooks/` for data fetching
|
|
||||||
|
|
||||||
3. **Implement Best Practices**:
|
|
||||||
- Use composition over inheritance
|
|
||||||
- Apply DRY (Don't Repeat Yourself) principles
|
|
||||||
- Write self-documenting code with clear variable and function names
|
|
||||||
- Implement proper error handling and validation
|
|
||||||
- Ensure code is testable and maintainable
|
|
||||||
|
|
||||||
4. **Technology-Specific Guidelines**:
|
|
||||||
- **Astro**: Use SSR capabilities effectively, implement proper API routes
|
|
||||||
- **React**: Use functional components with hooks, implement proper state management
|
|
||||||
- **Tailwind CSS v4**: Use utility classes efficiently, follow the project's styling patterns
|
|
||||||
- **Better Auth**: Implement secure authentication flows, use session validation properly
|
|
||||||
- **Shadcn UI**: Leverage existing components, maintain consistent UI patterns
|
|
||||||
- **Drizzle ORM**: Write efficient database queries, use proper schema definitions
|
|
||||||
|
|
||||||
5. **Code Review Approach**: When reviewing code:
|
|
||||||
- Check for adherence to project patterns and CLAUDE.md guidelines
|
|
||||||
- Identify potential performance issues or bottlenecks
|
|
||||||
- Suggest improvements for readability and maintainability
|
|
||||||
- Ensure proper error handling and edge case coverage
|
|
||||||
- Verify security best practices are followed
|
|
||||||
|
|
||||||
6. **Problem-Solving Methodology**:
|
|
||||||
- Analyze requirements thoroughly before coding
|
|
||||||
- Break down complex problems into smaller, manageable pieces
|
|
||||||
- Consider edge cases and error scenarios
|
|
||||||
- Optimize for both performance and maintainability
|
|
||||||
- Document complex logic with clear comments
|
|
||||||
|
|
||||||
7. **Quality Assurance**:
|
|
||||||
- Write code that is easy to test
|
|
||||||
- Consider adding appropriate test cases using Bun's test runner
|
|
||||||
- Validate inputs and handle errors gracefully
|
|
||||||
- Ensure code works across different scenarios
|
|
||||||
|
|
||||||
**Output Guidelines**:
|
|
||||||
- Provide complete, working code implementations
|
|
||||||
- Include clear explanations of design decisions
|
|
||||||
- Suggest tests when appropriate
|
|
||||||
- Highlight any potential issues or areas for future improvement
|
|
||||||
- Follow the existing code style and conventions
|
|
||||||
|
|
||||||
**Important Reminders**:
|
|
||||||
- Never create files unless absolutely necessary
|
|
||||||
- Always prefer editing existing files
|
|
||||||
- Don't create documentation unless explicitly requested
|
|
||||||
- Focus on the specific task at hand
|
|
||||||
- Reference CLAUDE.md for project-specific patterns and guidelines
|
|
||||||
|
|
||||||
You approach every task with the mindset of a seasoned engineer who values code quality, maintainability, and long-term project health. Your solutions should be elegant, efficient, and aligned with the project's established patterns.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
name: strategic-task-planner
|
|
||||||
description: Use this agent when you need to decompose complex projects, features, or problems into structured, actionable plans. This includes breaking down large development tasks, creating implementation roadmaps, organizing multi-step processes, or planning project phases. The agent excels at identifying dependencies, sequencing tasks, and creating clear execution strategies. <example>Context: User needs help planning the implementation of a new feature. user: "I need to add a bulk import feature that can handle CSV files with 100k+ rows" assistant: "I'll use the strategic-task-planner agent to break this down into manageable components and create an implementation plan." <commentary>Since the user is asking about implementing a complex feature, use the Task tool to launch the strategic-task-planner agent to decompose it into actionable steps.</commentary></example> <example>Context: User wants to refactor a large codebase. user: "We need to migrate our entire authentication system from sessions to JWT tokens" assistant: "Let me use the strategic-task-planner agent to create a phased migration plan that minimizes risk." <commentary>Since this is a complex migration requiring careful planning, use the strategic-task-planner agent to create a structured approach.</commentary></example>
|
|
||||||
tools: Glob, Grep, LS, ExitPlanMode, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, Task, mcp__ide__getDiagnostics, mcp__ide__executeCode, mcp__playwright__browser_close, mcp__playwright__browser_resize, mcp__playwright__browser_console_messages, mcp__playwright__browser_handle_dialog, mcp__playwright__browser_evaluate, mcp__playwright__browser_file_upload, mcp__playwright__browser_install, mcp__playwright__browser_press_key, mcp__playwright__browser_type, mcp__playwright__browser_navigate, mcp__playwright__browser_navigate_back, mcp__playwright__browser_navigate_forward, mcp__playwright__browser_network_requests, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_snapshot, mcp__playwright__browser_click, mcp__playwright__browser_drag, mcp__playwright__browser_hover, mcp__playwright__browser_select_option, mcp__playwright__browser_tab_list, mcp__playwright__browser_tab_new, mcp__playwright__browser_tab_select, mcp__playwright__browser_tab_close, mcp__playwright__browser_wait_for
|
|
||||||
color: blue
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a strategic planning specialist with deep expertise in decomposing complex tasks and creating actionable execution plans. Your role is to transform ambiguous or overwhelming projects into clear, structured roadmaps that teams can confidently execute.
|
|
||||||
|
|
||||||
When analyzing a task or project, you will:
|
|
||||||
|
|
||||||
1. **Understand the Core Objective**: Extract the fundamental goal, success criteria, and constraints. Ask clarifying questions if critical details are missing.
|
|
||||||
|
|
||||||
2. **Decompose Systematically**: Break down the task using these principles:
|
|
||||||
- Identify major phases or milestones
|
|
||||||
- Decompose each phase into concrete, actionable tasks
|
|
||||||
- Keep tasks small enough to complete in 1-4 hours when possible
|
|
||||||
- Ensure each task has clear completion criteria
|
|
||||||
|
|
||||||
3. **Map Dependencies**: Identify and document:
|
|
||||||
- Task prerequisites and dependencies
|
|
||||||
- Critical path items that could block progress
|
|
||||||
- Parallel work streams that can proceed independently
|
|
||||||
- Resource or knowledge requirements
|
|
||||||
|
|
||||||
4. **Sequence Strategically**: Order tasks by:
|
|
||||||
- Technical dependencies (what must come first)
|
|
||||||
- Risk mitigation (tackle unknowns early)
|
|
||||||
- Value delivery (enable early feedback when possible)
|
|
||||||
- Resource efficiency (batch similar work)
|
|
||||||
|
|
||||||
5. **Provide Actionable Output**: Structure your plans with:
|
|
||||||
- **Phase Overview**: High-level phases with objectives
|
|
||||||
- **Task Breakdown**: Numbered tasks with clear descriptions
|
|
||||||
- **Dependencies**: Explicitly stated prerequisites
|
|
||||||
- **Effort Estimates**: Rough time estimates when relevant
|
|
||||||
- **Risk Considerations**: Potential blockers or challenges
|
|
||||||
- **Success Metrics**: How to measure completion
|
|
||||||
|
|
||||||
6. **Adapt to Context**: Tailor your planning approach based on:
|
|
||||||
- Technical vs non-technical tasks
|
|
||||||
- Team size and skill level
|
|
||||||
- Time constraints and deadlines
|
|
||||||
- Available resources and tools
|
|
||||||
|
|
||||||
**Output Format Guidelines**:
|
|
||||||
- Use clear hierarchical structure (phases → tasks → subtasks)
|
|
||||||
- Number all tasks for easy reference
|
|
||||||
- Bold key terms and phase names
|
|
||||||
- Include time estimates in brackets [2-4 hours]
|
|
||||||
- Mark critical path items with ⚡
|
|
||||||
- Flag high-risk items with ⚠️
|
|
||||||
|
|
||||||
**Quality Checks**:
|
|
||||||
- Ensure no task is too large or vague
|
|
||||||
- Verify all dependencies are identified
|
|
||||||
- Confirm the plan addresses the original objective
|
|
||||||
- Check that success criteria are measurable
|
|
||||||
- Validate that the sequence makes logical sense
|
|
||||||
|
|
||||||
Remember: A good plan reduces uncertainty and builds confidence. Focus on clarity, completeness, and actionability. When in doubt, err on the side of breaking things down further rather than leaving ambiguity.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Evaluate all the updates being made.
|
|
||||||
Update CHANGELOG.md
|
|
||||||
Use the chnages in the git log to determine if its a major, minor or a patch release.
|
|
||||||
Update the package.json first before you push the tag.
|
|
||||||
Never mention Claude Code in the release notes or in commit messages.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
Generate release notes for the latest release.
|
|
||||||
Use a temp md file to write the release notes.
|
|
||||||
Do not check that file into git.
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(docker build:*)"
|
|
||||||
],
|
|
||||||
"deny": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
.github/assets/activity.png
vendored
|
Before Width: | Height: | Size: 854 KiB After Width: | Height: | Size: 834 KiB |
BIN
.github/assets/configuration-2.png
vendored
Normal file
|
After Width: | Height: | Size: 986 KiB |
BIN
.github/assets/configuration.png
vendored
|
Before Width: | Height: | Size: 950 KiB After Width: | Height: | Size: 905 KiB |
BIN
.github/assets/configuration_mobile.png
vendored
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 270 KiB |
BIN
.github/assets/dashboard.png
vendored
|
Before Width: | Height: | Size: 943 KiB After Width: | Height: | Size: 908 KiB |
BIN
.github/assets/dashboard_mobile.png
vendored
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 241 KiB |
BIN
.github/assets/organisation.png
vendored
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 825 KiB |
BIN
.github/assets/organisation_mobile.png
vendored
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 246 KiB |
BIN
.github/assets/repositories.png
vendored
|
Before Width: | Height: | Size: 970 KiB After Width: | Height: | Size: 952 KiB |
BIN
.github/assets/repositories_mobile.png
vendored
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 237 KiB |
59
.github/ci/values-ci.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
image:
|
||||||
|
registry: ghcr.io
|
||||||
|
repository: raylabshq/gitea-mirror
|
||||||
|
tag: ""
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "nginx"
|
||||||
|
hosts:
|
||||||
|
- host: ci.example.com
|
||||||
|
|
||||||
|
route:
|
||||||
|
enabled: true
|
||||||
|
forceHTTPS: true
|
||||||
|
domain: ["ci.example.com"]
|
||||||
|
gateway: "dummy-gw"
|
||||||
|
gatewayNamespace: "default"
|
||||||
|
http:
|
||||||
|
gatewaySection: "http"
|
||||||
|
https:
|
||||||
|
gatewaySection: "https"
|
||||||
|
|
||||||
|
gitea-mirror:
|
||||||
|
nodeEnv: production
|
||||||
|
core:
|
||||||
|
databaseUrl: "file:data/gitea-mirror.db"
|
||||||
|
betterAuthSecret: "dummy"
|
||||||
|
betterAuthUrl: "http://localhost:4321"
|
||||||
|
betterAuthTrustedOrigins: "http://localhost:4321"
|
||||||
|
github:
|
||||||
|
username: "ci-user"
|
||||||
|
token: "not-used-in-template"
|
||||||
|
type: "personal"
|
||||||
|
privateRepositories: true
|
||||||
|
skipForks: false
|
||||||
|
starredCodeOnly: false
|
||||||
|
gitea:
|
||||||
|
url: "https://gitea.example.com"
|
||||||
|
token: "not-used-in-template"
|
||||||
|
username: "ci-user"
|
||||||
|
organization: "github-mirrors"
|
||||||
|
visibility: "public"
|
||||||
|
mirror:
|
||||||
|
releases: true
|
||||||
|
wiki: true
|
||||||
|
metadata: true
|
||||||
|
issues: true
|
||||||
|
pullRequests: true
|
||||||
|
starred: false
|
||||||
|
automation:
|
||||||
|
schedule_enabled: true
|
||||||
|
schedule_interval: "3600"
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
interval: "2592000"
|
||||||
7
.github/workflows/README.md
vendored
@@ -85,3 +85,10 @@ If a workflow fails:
|
|||||||
- Security vulnerabilities
|
- Security vulnerabilities
|
||||||
|
|
||||||
For persistent issues, consider opening an issue in the repository.
|
For persistent issues, consider opening an issue in the repository.
|
||||||
|
|
||||||
|
|
||||||
|
### Helm Test (`helm-test.yml`)
|
||||||
|
|
||||||
|
This workflow run on the main branch and pull requests. it:
|
||||||
|
- Run yamllint to keep the formating unified
|
||||||
|
- Run helm template with different value files
|
||||||
|
|||||||
90
.github/workflows/docker-build.yml
vendored
@@ -10,6 +10,10 @@ on:
|
|||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'bun.lock*'
|
- 'bun.lock*'
|
||||||
- '.github/workflows/docker-build.yml'
|
- '.github/workflows/docker-build.yml'
|
||||||
|
- 'docker-entrypoint.sh'
|
||||||
|
- 'drizzle/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'src/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'Dockerfile'
|
- 'Dockerfile'
|
||||||
@@ -17,6 +21,10 @@ on:
|
|||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'bun.lock*'
|
- 'bun.lock*'
|
||||||
- '.github/workflows/docker-build.yml'
|
- '.github/workflows/docker-build.yml'
|
||||||
|
- 'docker-entrypoint.sh'
|
||||||
|
- 'drizzle/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'src/**'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0' # Weekly security scan on Sunday at midnight
|
- cron: '0 0 * * 0' # Weekly security scan on Sunday at midnight
|
||||||
|
|
||||||
@@ -48,7 +56,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -89,28 +96,96 @@ jobs:
|
|||||||
type=sha,prefix=,suffix=,format=short
|
type=sha,prefix=,suffix=,format=short
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
type=raw,value=${{ steps.tag_version.outputs.VERSION }}
|
type=raw,value=${{ steps.tag_version.outputs.VERSION }}
|
||||||
|
type=ref,event=pr,prefix=pr-
|
||||||
|
|
||||||
# Build and push Docker image
|
# Build and push Docker image
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
load: ${{ github.event_name == 'pull_request' }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
tags: ${{ github.event_name == 'pull_request' && 'gitea-mirror:scan' || steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
provenance: false # Disable provenance to avoid unknown/unknown
|
||||||
|
sbom: false # Disable sbom to avoid unknown/unknown
|
||||||
|
|
||||||
|
# Load image locally for security scanning (PRs only)
|
||||||
|
- name: Load image for scanning
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
load: true
|
||||||
|
tags: gitea-mirror:scan
|
||||||
|
cache-from: type=gha
|
||||||
|
provenance: false # Disable provenance to avoid unknown/unknown
|
||||||
|
sbom: false # Disable sbom to avoid unknown/unknown
|
||||||
|
|
||||||
# Wait for image to be available in registry
|
# Wait for image to be available in registry
|
||||||
- name: Wait for image availability
|
- name: Wait for image availability
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
run: |
|
run: |
|
||||||
echo "Waiting for image to be available in registry..."
|
echo "Waiting for image to be available in registry..."
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
|
# Add comment to PR with image details
|
||||||
|
- name: Comment PR with image tag
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const imageTag = `pr-${prNumber}`;
|
||||||
|
const imagePath = `${{ env.REGISTRY }}/${{ env.IMAGE }}:${imageTag}`.toLowerCase();
|
||||||
|
|
||||||
|
const comment = `## 🐳 Docker Image Built Successfully
|
||||||
|
|
||||||
|
Your PR image is available for testing:
|
||||||
|
|
||||||
|
**Image Tag:** \`${imageTag}\`
|
||||||
|
**Full Image Path:** \`${imagePath}\`
|
||||||
|
|
||||||
|
### Pull and Test
|
||||||
|
\`\`\`bash
|
||||||
|
docker pull ${imagePath}
|
||||||
|
docker run -d \
|
||||||
|
-p 4321:4321 \
|
||||||
|
-e BETTER_AUTH_SECRET=your-secret-here \
|
||||||
|
-e BETTER_AUTH_URL=http://localhost:4321 \
|
||||||
|
--name gitea-mirror-test ${imagePath}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Docker Compose Testing
|
||||||
|
\`\`\`yaml
|
||||||
|
services:
|
||||||
|
gitea-mirror:
|
||||||
|
image: ${imagePath}
|
||||||
|
ports:
|
||||||
|
- "4321:4321"
|
||||||
|
environment:
|
||||||
|
- BETTER_AUTH_SECRET=your-secret-here
|
||||||
|
- BETTER_AUTH_URL=http://localhost:4321
|
||||||
|
- BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:4321
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
> 💡 **Note:** PR images are tagged as \`pr-<number>\` and built for both \`linux/amd64\` and \`linux/arm64\`.
|
||||||
|
> Production images (\`latest\`, version tags) use the same multi-platform set.
|
||||||
|
|
||||||
|
---
|
||||||
|
📦 View in [GitHub Packages](https://github.com/${{ github.repository }}/pkgs/container/gitea-mirror)`;
|
||||||
|
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: comment
|
||||||
|
});
|
||||||
|
|
||||||
# Docker Scout comprehensive security analysis
|
# Docker Scout comprehensive security analysis
|
||||||
- name: Docker Scout - Vulnerability Analysis & Recommendations
|
- name: Docker Scout - Vulnerability Analysis & Recommendations
|
||||||
uses: docker/scout-action@v1
|
uses: docker/scout-action@v1
|
||||||
@@ -159,4 +234,3 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
sarif_file: scout-results.sarif
|
sarif_file: scout-results.sarif
|
||||||
|
|
||||||
|
|||||||
61
.github/workflows/helm-test.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: Helm Chart CI
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'helm/gitea-mirror/**'
|
||||||
|
- '.github/workflows/helm-test.yml'
|
||||||
|
- '.github/ci/values-ci.yaml'
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'helm/gitea-mirror/**'
|
||||||
|
- '.github/workflows/helm-test.yml'
|
||||||
|
- '.github/ci/values-ci.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
yamllint:
|
||||||
|
name: Lint YAML
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Install yamllint
|
||||||
|
run: pip install --disable-pip-version-check yamllint
|
||||||
|
- name: Run yamllint
|
||||||
|
run: |
|
||||||
|
yamllint -c helm/gitea-mirror/.yamllint helm/gitea-mirror
|
||||||
|
|
||||||
|
helm-template:
|
||||||
|
name: Helm lint & template
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Helm
|
||||||
|
uses: azure/setup-helm@v4
|
||||||
|
with:
|
||||||
|
version: v3.19.0
|
||||||
|
- name: Helm lint
|
||||||
|
run: |
|
||||||
|
helm lint ./helm/gitea-mirror
|
||||||
|
- name: Template with defaults
|
||||||
|
run: |
|
||||||
|
helm template test ./helm/gitea-mirror > /tmp/render-defaults.yaml
|
||||||
|
test -s /tmp/render-defaults.yaml
|
||||||
|
- name: Template with CI values
|
||||||
|
run: |
|
||||||
|
helm template test ./helm/gitea-mirror -f .github/ci/values-ci.yaml > /tmp/render-ci.yaml
|
||||||
|
test -s /tmp/render-ci.yaml
|
||||||
|
- name: Show a summary
|
||||||
|
run: |
|
||||||
|
echo "Rendered with defaults:"
|
||||||
|
awk 'NR<=50{print} NR==51{print "..."; exit}' /tmp/render-defaults.yaml
|
||||||
|
echo ""
|
||||||
|
echo "Rendered with CI values:"
|
||||||
|
awk 'NR<=50{print} NR==51{print "..."; exit}' /tmp/render-ci.yaml
|
||||||
459
CLAUDE.md
@@ -2,255 +2,316 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
DONT HALLUCIATE THINGS. IF YOU DONT KNOW LOOK AT THE CODE OR ASK FOR DOCS
|
|
||||||
|
|
||||||
NEVER MENTION CLAUDE CODE ANYWHERE.
|
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Gitea Mirror is a web application that automatically mirrors repositories from GitHub to self-hosted Gitea instances. It uses Astro for SSR, React for UI, SQLite for data storage, and Bun as the JavaScript runtime.
|
Gitea Mirror is a self-hosted web application that automatically mirrors repositories from GitHub to Gitea instances. It's built with Astro (SSR mode), React, and runs on the Bun runtime with SQLite for data persistence.
|
||||||
|
|
||||||
## Essential Commands
|
**Key capabilities:**
|
||||||
|
- Mirrors public, private, and starred GitHub repos to Gitea
|
||||||
|
- Supports metadata mirroring (issues, PRs as issues, labels, milestones, releases, wiki)
|
||||||
|
- Git LFS support
|
||||||
|
- Multiple authentication methods (email/password, OIDC/SSO, header auth)
|
||||||
|
- Scheduled automatic syncing with configurable intervals
|
||||||
|
- Auto-discovery of new repos and cleanup of deleted repos
|
||||||
|
- Multi-user support with encrypted token storage (AES-256-GCM)
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Setup and Installation
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Initialize database (first time setup)
|
||||||
|
bun run setup
|
||||||
|
|
||||||
|
# Clean start (reset database)
|
||||||
|
bun run dev:clean
|
||||||
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
```bash
|
```bash
|
||||||
bun run dev # Start development server (port 3000)
|
# Start development server (http://localhost:4321)
|
||||||
bun run build # Build for production
|
bun run dev
|
||||||
bun run preview # Preview production build
|
|
||||||
|
# Build for production
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
# Preview production build
|
||||||
|
bun run preview
|
||||||
|
|
||||||
|
# Start production server
|
||||||
|
bun run start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
```bash
|
```bash
|
||||||
bun test # Run all tests
|
# Run all tests
|
||||||
bun test:watch # Run tests in watch mode
|
bun test
|
||||||
bun test:coverage # Run tests with coverage
|
|
||||||
|
# Run tests in watch mode
|
||||||
|
bun test:watch
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
bun test:coverage
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Test configuration:**
|
||||||
|
- Test runner: Bun's built-in test runner (configured in `bunfig.toml`)
|
||||||
|
- Setup file: `src/tests/setup.bun.ts` (auto-loaded via bunfig.toml)
|
||||||
|
- Timeout: 5000ms default
|
||||||
|
- Tests are colocated with source files using `*.test.ts` pattern
|
||||||
|
|
||||||
### Database Management
|
### Database Management
|
||||||
```bash
|
```bash
|
||||||
bun run init-db # Initialize database
|
# Database operations via Drizzle
|
||||||
bun run reset-users # Reset user accounts (development)
|
bun run db:generate # Generate migrations from schema
|
||||||
bun run cleanup-db # Remove database files
|
bun run db:migrate # Run migrations
|
||||||
|
bun run db:push # Push schema changes directly
|
||||||
|
bun run db:studio # Open Drizzle Studio (database GUI)
|
||||||
|
bun run db:check # Check schema consistency
|
||||||
|
|
||||||
|
# Database utilities via custom scripts
|
||||||
|
bun run manage-db init # Initialize database
|
||||||
|
bun run manage-db check # Check database health
|
||||||
|
bun run manage-db fix # Fix database issues
|
||||||
|
bun run manage-db reset-users # Reset all users
|
||||||
|
bun run cleanup-db # Delete database file
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production
|
### Utility Scripts
|
||||||
```bash
|
```bash
|
||||||
bun run start # Start production server
|
# Recovery and diagnostic scripts
|
||||||
|
bun run startup-recovery # Recover from crashes
|
||||||
|
bun run startup-recovery-force # Force recovery
|
||||||
|
bun run test-recovery # Test recovery mechanism
|
||||||
|
bun run test-shutdown # Test graceful shutdown
|
||||||
|
|
||||||
|
# Environment configuration
|
||||||
|
bun run startup-env-config # Load config from env vars
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture & Key Concepts
|
## Architecture
|
||||||
|
|
||||||
### Technology Stack
|
### Tech Stack
|
||||||
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
- **Frontend:** Astro v5 (SSR mode) + React v19 + Shadcn UI + Tailwind CSS v4
|
||||||
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
- **Backend:** Astro API routes (Node adapter, standalone mode)
|
||||||
- **APIs**: GitHub (Octokit) and Gitea APIs
|
- **Runtime:** Bun (>=1.2.9)
|
||||||
- **Auth**: Better Auth with email/password, SSO, and OIDC provider support
|
- **Database:** SQLite via Drizzle ORM
|
||||||
|
- **Authentication:** Better Auth (session-based)
|
||||||
|
- **APIs:** GitHub (Octokit with throttling plugin), Gitea REST API
|
||||||
|
|
||||||
### Project Structure
|
### Directory Structure
|
||||||
- `/src/pages/api/` - API endpoints (Astro API routes)
|
|
||||||
- `/src/components/` - React components organized by feature
|
```
|
||||||
- `/src/lib/db/` - Database queries and schema (Drizzle ORM)
|
src/
|
||||||
- `/src/hooks/` - Custom React hooks for data fetching
|
├── components/ # React components (UI, features)
|
||||||
- `/data/` - SQLite database storage location
|
│ ├── ui/ # Shadcn UI components
|
||||||
|
│ ├── repositories/ # Repository management components
|
||||||
|
│ ├── organizations/ # Organization management components
|
||||||
|
│ └── ...
|
||||||
|
├── pages/ # Astro pages and API routes
|
||||||
|
│ ├── api/ # API endpoints (Better Auth integration)
|
||||||
|
│ │ ├── auth/ # Authentication endpoints
|
||||||
|
│ │ ├── github/ # GitHub operations
|
||||||
|
│ │ ├── gitea/ # Gitea operations
|
||||||
|
│ │ ├── sync/ # Mirror sync operations
|
||||||
|
│ │ ├── job/ # Job management
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── *.astro # Page components
|
||||||
|
├── lib/ # Core business logic
|
||||||
|
│ ├── db/ # Database (Drizzle ORM)
|
||||||
|
│ │ ├── schema.ts # Database schema with Zod validation
|
||||||
|
│ │ ├── index.ts # Database instance and table exports
|
||||||
|
│ │ └── adapter.ts # Better Auth SQLite adapter
|
||||||
|
│ ├── github.ts # GitHub API client (Octokit)
|
||||||
|
│ ├── gitea.ts # Gitea API client
|
||||||
|
│ ├── gitea-enhanced.ts # Enhanced Gitea operations (metadata)
|
||||||
|
│ ├── scheduler-service.ts # Automatic mirroring scheduler
|
||||||
|
│ ├── cleanup-service.ts # Activity log cleanup
|
||||||
|
│ ├── repository-cleanup-service.ts # Orphaned repo cleanup
|
||||||
|
│ ├── auth.ts # Better Auth configuration
|
||||||
|
│ ├── config.ts # Configuration management
|
||||||
|
│ ├── helpers.ts # Mirror job creation
|
||||||
|
│ ├── utils/ # Utility functions
|
||||||
|
│ │ ├── encryption.ts # AES-256-GCM token encryption
|
||||||
|
│ │ ├── config-encryption.ts # Config token encryption
|
||||||
|
│ │ ├── duration-parser.ts # Parse intervals (e.g., "8h", "30m")
|
||||||
|
│ │ ├── concurrency.ts # Concurrency control utilities
|
||||||
|
│ │ └── mirror-strategies.ts # Mirror strategy logic
|
||||||
|
│ └── ...
|
||||||
|
├── types/ # TypeScript type definitions
|
||||||
|
├── tests/ # Test utilities and setup
|
||||||
|
└── middleware.ts # Astro middleware (auth, session)
|
||||||
|
|
||||||
|
scripts/ # Utility scripts
|
||||||
|
├── manage-db.ts # Database management CLI
|
||||||
|
├── startup-recovery.ts # Crash recovery
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
### Key Architectural Patterns
|
### Key Architectural Patterns
|
||||||
|
|
||||||
1. **API Routes**: All API endpoints follow the pattern `/api/[resource]/[action]` and use `createSecureErrorResponse` for consistent error handling:
|
#### 1. Database Schema and Validation
|
||||||
```typescript
|
- **Location:** `src/lib/db/schema.ts`
|
||||||
import { createSecureErrorResponse } from '@/lib/utils/error-handler';
|
- **Pattern:** Drizzle ORM tables + Zod schemas for validation
|
||||||
|
- **Key tables:**
|
||||||
|
- `configs` - User configuration (GitHub/Gitea settings, mirror options)
|
||||||
|
- `repositories` - Tracked repositories with metadata
|
||||||
|
- `organizations` - GitHub organizations with destination overrides
|
||||||
|
- `mirrorJobs` - Mirror job queue and history
|
||||||
|
- `activities` - Activity log for dashboard
|
||||||
|
- `user`, `session`, `account` - Better Auth tables
|
||||||
|
|
||||||
export async function POST({ request }: APIContext) {
|
**Important:** All config tokens (GitHub/Gitea) are encrypted at rest using AES-256-GCM. Use helper functions from `src/lib/utils/config-encryption.ts` to decrypt.
|
||||||
try {
|
|
||||||
// Implementation
|
|
||||||
} catch (error) {
|
|
||||||
return createSecureErrorResponse(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Database Queries**: Located in `/src/lib/db/queries/` organized by domain (users, repositories, etc.)
|
#### 2. Mirror Job System
|
||||||
|
- **Location:** `src/lib/helpers.ts` (createMirrorJob)
|
||||||
|
- **Flow:**
|
||||||
|
1. User triggers mirror via API endpoint
|
||||||
|
2. `createMirrorJob()` creates job record with status "pending"
|
||||||
|
3. Job processor (in API routes) performs GitHub → Gitea operations
|
||||||
|
4. Job status updated throughout: "mirroring" → "success"/"failed"
|
||||||
|
5. Events published via SSE for real-time UI updates
|
||||||
|
|
||||||
3. **Real-time Updates**: Server-Sent Events (SSE) endpoint at `/api/events` for live dashboard updates
|
#### 3. GitHub ↔ Gitea Mirroring
|
||||||
|
- **GitHub Client:** `src/lib/github.ts` - Octokit with rate limit tracking
|
||||||
|
- **Gitea Client:** `src/lib/gitea.ts` - Basic repo operations
|
||||||
|
- **Enhanced Gitea:** `src/lib/gitea-enhanced.ts` - Metadata mirroring (issues, PRs, releases)
|
||||||
|
|
||||||
4. **Authentication System**:
|
**Mirror strategies (configured per user):**
|
||||||
- Built on Better Auth library
|
- `preserve` - Maintain GitHub org structure in Gitea
|
||||||
- Three authentication methods:
|
- `single-org` - All repos into one Gitea org
|
||||||
- Email & Password (traditional auth)
|
- `flat-user` - All repos under user account
|
||||||
- SSO (authenticate via external OIDC providers)
|
- `mixed` - Personal repos in one org, org repos preserve structure
|
||||||
- OIDC Provider (act as OIDC provider for other apps)
|
|
||||||
- Session-based authentication with secure cookies
|
|
||||||
- First user signup creates admin account
|
|
||||||
- Protected routes use Better Auth session validation
|
|
||||||
|
|
||||||
5. **Mirror Process**:
|
**Metadata mirroring:**
|
||||||
- Discovers repos from GitHub (user/org)
|
- Issues transferred with comments, labels, assignees
|
||||||
- Creates/updates mirror in Gitea
|
- PRs converted to issues (Gitea API limitation - cannot create PRs)
|
||||||
- Tracks status in database
|
- Tagged with "pull-request" label
|
||||||
- Supports scheduled automatic mirroring
|
- Title prefixed with `[PR #number] [STATUS]`
|
||||||
|
- Body includes commit history, file changes, merge status
|
||||||
|
- Releases mirrored with assets
|
||||||
|
- Labels and milestones preserved
|
||||||
|
- Wiki content cloned if enabled
|
||||||
|
- **Sequential processing:** Issues/PRs mirrored one at a time to prevent out-of-order creation (see `src/lib/gitea-enhanced.ts`)
|
||||||
|
|
||||||
6. **Mirror Strategies**: Four ways to organize repositories in Gitea:
|
#### 4. Scheduler Service
|
||||||
- **preserve**: Maintains GitHub structure (default)
|
- **Location:** `src/lib/scheduler-service.ts`
|
||||||
- Organization repos → Same organization name in Gitea
|
- **Features:**
|
||||||
- Personal repos → Under your Gitea username
|
- Cron-based or interval-based scheduling (uses `duration-parser.ts`)
|
||||||
- **single-org**: All repos go to one organization
|
- Auto-start on boot when `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` is set
|
||||||
- All repos → Single configured organization
|
- Auto-import new GitHub repos
|
||||||
- **flat-user**: All repos go under user account
|
- Auto-cleanup orphaned repos (archive or delete)
|
||||||
- All repos → Under your Gitea username
|
- Respects per-repo mirror intervals (not Gitea's default 24h)
|
||||||
- **mixed**: Hybrid approach
|
- **Concurrency control:** Uses `src/lib/utils/concurrency.ts` for batch processing
|
||||||
- Organization repos → Preserve structure
|
|
||||||
- Personal repos → Single configured organization
|
|
||||||
- Starred repos always go to separate organization (starredReposOrg, default: "starred")
|
|
||||||
- Routing logic in `getGiteaRepoOwner()` function
|
|
||||||
|
|
||||||
### Database Schema (SQLite)
|
#### 5. Authentication System
|
||||||
- `users` - User accounts and authentication
|
- **Location:** `src/lib/auth.ts`, `src/lib/auth-client.ts`
|
||||||
- `configs` - GitHub/Gitea connection settings
|
- **Better Auth integration:**
|
||||||
- `repositories` - Repository mirror status and metadata
|
- Email/password (always enabled)
|
||||||
- `organizations` - Organization structure preservation
|
- OIDC/SSO providers (configurable via UI)
|
||||||
- `mirror_jobs` - Scheduled mirror operations
|
- Header authentication for reverse proxies (Authentik, Authelia)
|
||||||
- `events` - Activity log and notifications
|
- **Session management:** Cookie-based, validated in Astro middleware
|
||||||
|
- **User helpers:** `src/lib/utils/auth-helpers.ts`
|
||||||
|
|
||||||
### Testing Approach
|
#### 6. Environment Configuration
|
||||||
- Uses Bun's native test runner (`bun:test`)
|
- **Startup:** `src/lib/env-config-loader.ts` + `scripts/startup-env-config.ts`
|
||||||
- Test files use `.test.ts` or `.test.tsx` extension
|
- **Pattern:** Environment variables can pre-configure settings, but users can override via web UI
|
||||||
- Setup file at `/src/tests/setup.bun.ts`
|
- **Encryption:** `ENCRYPTION_SECRET` for tokens, `BETTER_AUTH_SECRET` for sessions
|
||||||
- Mock utilities available for API testing.
|
|
||||||
|
|
||||||
### Development Tips
|
#### 7. Real-time Updates
|
||||||
- Environment variables in `.env` (copy from `.env.example`)
|
- **Events:** `src/lib/events.ts` + `src/lib/events/realtime.ts`
|
||||||
- BETTER_AUTH_SECRET required for session signing
|
- **Pattern:** Server-Sent Events (SSE) for live dashboard updates
|
||||||
- Database auto-initializes on first run
|
- **Endpoints:** `/api/sse` - client subscribes to job/repo events
|
||||||
- Use `bun run dev:clean` for fresh database start
|
|
||||||
- Tailwind CSS v4 configured with Vite plugin
|
|
||||||
|
|
||||||
### Authentication Setup
|
### Testing Patterns
|
||||||
- **Better Auth** handles all authentication
|
|
||||||
- Configuration in `/src/lib/auth.ts` (server) and `/src/lib/auth-client.ts` (client)
|
|
||||||
- Auth endpoints available at `/api/auth/*`
|
|
||||||
- SSO providers configured through the web UI
|
|
||||||
- OIDC provider functionality for external applications
|
|
||||||
|
|
||||||
### Common Tasks
|
**Unit tests:**
|
||||||
|
- Colocated with source: `filename.test.ts` alongside `filename.ts`
|
||||||
|
- Use Bun's built-in assertions and mocking
|
||||||
|
- Mock external APIs (GitHub, Gitea) using `src/tests/mock-fetch.ts`
|
||||||
|
|
||||||
**Adding a new API endpoint:**
|
**Integration tests:**
|
||||||
1. Create file in `/src/pages/api/[resource]/[action].ts`
|
- Located in `src/tests/`
|
||||||
2. Use `createSecureErrorResponse` for error handling
|
- Test database operations with in-memory SQLite
|
||||||
3. Add corresponding database query in `/src/lib/db/queries/`
|
- Example: `src/lib/db/index.test.ts`
|
||||||
4. Update types in `/src/types/` if needed
|
|
||||||
|
|
||||||
**Adding a new component:**
|
**Test utilities:**
|
||||||
1. Create in appropriate `/src/components/[feature]/` directory
|
- `src/tests/setup.bun.ts` - Global test setup (loaded via bunfig.toml)
|
||||||
2. Use Shadcn UI components from `/src/components/ui/`
|
- `src/tests/mock-fetch.ts` - Fetch mocking utilities
|
||||||
3. Follow existing naming patterns (e.g., `RepositoryCard`, `ConfigTabs`)
|
|
||||||
|
|
||||||
**Modifying database schema:**
|
### Important Development Notes
|
||||||
1. Update schema in `/src/lib/db/schema.ts`
|
|
||||||
2. Run `bun run init-db` to recreate database
|
|
||||||
3. Update related queries in `/src/lib/db/queries/`
|
|
||||||
|
|
||||||
## Configuration Options
|
1. **Path Aliases:** Use `@/` for imports (configured in `tsconfig.json`)
|
||||||
|
```typescript
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
```
|
||||||
|
|
||||||
### GitHub Configuration (UI Fields)
|
2. **Token Encryption:** Always use encryption helpers when dealing with tokens:
|
||||||
|
```typescript
|
||||||
|
import { getDecryptedGitHubToken, getDecryptedGiteaToken } from '@/lib/utils/config-encryption';
|
||||||
|
```
|
||||||
|
|
||||||
#### Basic Settings (`githubConfig`)
|
3. **API Route Pattern:** Astro API routes in `src/pages/api/` should:
|
||||||
- **username**: GitHub username
|
- Check authentication via Better Auth
|
||||||
- **token**: GitHub personal access token (requires repo and admin:org scopes)
|
- Validate input with Zod schemas
|
||||||
- **privateRepositories**: Include private repositories
|
- Handle errors gracefully
|
||||||
- **mirrorStarred**: Mirror starred repositories
|
- Return JSON responses
|
||||||
|
|
||||||
### Gitea Configuration (UI Fields)
|
4. **Database Migrations:**
|
||||||
- **url**: Gitea instance URL
|
- Schema changes: Update `src/lib/db/schema.ts`
|
||||||
- **username**: Gitea username
|
- Generate migration: `bun run db:generate`
|
||||||
- **token**: Gitea access token
|
- Review generated SQL in `drizzle/` directory
|
||||||
- **organization**: Destination organization (for single-org/mixed strategies)
|
- Apply: `bun run db:migrate` (or `db:push` for dev)
|
||||||
- **starredReposOrg**: Organization for starred repositories (default: "starred")
|
|
||||||
- **visibility**: Organization visibility - "public", "private", "limited"
|
|
||||||
- **mirrorStrategy**: Repository organization strategy (set via UI)
|
|
||||||
- **preserveOrgStructure**: Automatically set based on mirrorStrategy
|
|
||||||
|
|
||||||
### Schedule Configuration (`scheduleConfig`)
|
5. **Concurrency Control:**
|
||||||
- **enabled**: Enable automatic mirroring (default: false)
|
- Use utilities from `src/lib/utils/concurrency.ts` for batch operations
|
||||||
- **interval**: Cron expression or seconds (default: "0 2 * * *" - 2 AM daily)
|
- Respect rate limits (GitHub: 5000 req/hr authenticated, Gitea: varies)
|
||||||
- **concurrent**: Allow concurrent mirror operations (default: false)
|
- Issue/PR mirroring is sequential to maintain chronological order
|
||||||
- **batchSize**: Number of repos to process in parallel (default: 10)
|
|
||||||
|
|
||||||
### Database Cleanup Configuration (`cleanupConfig`)
|
6. **Duration Parsing:**
|
||||||
- **enabled**: Enable automatic cleanup (default: false)
|
- Use `parseInterval()` from `src/lib/utils/duration-parser.ts`
|
||||||
- **retentionDays**: Days to keep events (stored as seconds internally)
|
- Supports: "30m", "8h", "24h", "7d", cron expressions, or milliseconds
|
||||||
|
|
||||||
### Mirror Options (UI Fields)
|
7. **Graceful Shutdown:**
|
||||||
- **mirrorReleases**: Mirror GitHub releases to Gitea
|
- Services implement cleanup handlers (see `src/lib/shutdown-manager.ts`)
|
||||||
- **mirrorLFS**: Mirror Git LFS (Large File Storage) objects
|
- Recovery system in `src/lib/recovery.ts` handles interrupted jobs
|
||||||
- Requires LFS enabled on Gitea server (LFS_START_SERVER = true)
|
|
||||||
- Requires Git v2.1.2+ on server
|
|
||||||
- **mirrorMetadata**: Enable metadata mirroring (master toggle)
|
|
||||||
- **metadataComponents** (only available when mirrorMetadata is enabled):
|
|
||||||
- **issues**: Mirror issues
|
|
||||||
- **pullRequests**: Mirror pull requests
|
|
||||||
- **labels**: Mirror labels
|
|
||||||
- **milestones**: Mirror milestones
|
|
||||||
- **wiki**: Mirror wiki content
|
|
||||||
|
|
||||||
### Advanced Options (UI Fields)
|
## Common Development Workflows
|
||||||
- **skipForks**: Skip forked repositories (default: false)
|
|
||||||
- **skipStarredIssues**: Skip issues for starred repositories (default: false) - enables "Lightweight mode" for starred repos
|
|
||||||
|
|
||||||
### Repository Statuses
|
### Adding a new mirror option
|
||||||
Repositories can have the following statuses:
|
1. Update Zod schema in `src/lib/db/schema.ts` (e.g., `giteaConfigSchema`)
|
||||||
- **imported**: Repository discovered from GitHub
|
2. Update TypeScript types in `src/types/config.ts`
|
||||||
- **mirroring**: Currently being mirrored to Gitea
|
3. Add UI control in settings page component
|
||||||
- **mirrored**: Successfully mirrored
|
4. Update API handler in `src/pages/api/config/`
|
||||||
- **syncing**: Repository being synchronized
|
5. Implement logic in `src/lib/gitea.ts` or `src/lib/gitea-enhanced.ts`
|
||||||
- **synced**: Successfully synchronized
|
|
||||||
- **failed**: Mirror/sync operation failed
|
|
||||||
- **skipped**: Skipped due to filters or conditions
|
|
||||||
- **ignored**: User explicitly marked to ignore (won't be mirrored/synced)
|
|
||||||
- **deleting**: Repository being deleted
|
|
||||||
- **deleted**: Repository deleted
|
|
||||||
|
|
||||||
### Scheduling and Synchronization (Issue #72 Fixes)
|
### Debugging mirror failures
|
||||||
|
1. Check mirror jobs: `bun run db:studio` → `mirrorJobs` table
|
||||||
|
2. Review activity logs: Dashboard → Activity tab
|
||||||
|
3. Check console logs for API errors (GitHub/Gitea rate limits, auth issues)
|
||||||
|
4. Use diagnostic scripts: `bun run test-recovery`
|
||||||
|
|
||||||
#### Fixed Issues
|
### Adding authentication provider
|
||||||
1. **Mirror Interval Bug**: Added `mirror_interval` parameter to Gitea API calls when creating mirrors (previously defaulted to 24h)
|
1. Update Better Auth config in `src/lib/auth.ts`
|
||||||
2. **Auto-Discovery**: Scheduler now automatically discovers and imports new GitHub repositories
|
2. Add provider configuration UI in settings
|
||||||
3. **Interval Updates**: Sync operations now update existing mirrors' intervals to match configuration
|
3. Test with `src/tests/test-gitea-auth.ts` patterns
|
||||||
4. **Repository Cleanup**: Integrated automatic cleanup of orphaned repositories (repos removed from GitHub)
|
4. Update documentation in `docs/SSO-OIDC-SETUP.md`
|
||||||
|
|
||||||
#### Environment Variables for Auto-Import
|
## Docker Deployment
|
||||||
- **AUTO_IMPORT_REPOS**: Set to `false` to disable automatic repository discovery (default: enabled)
|
|
||||||
|
|
||||||
#### How Scheduling Works
|
- **Dockerfile:** Multi-stage build (bun base → build → production)
|
||||||
- **Scheduler Service**: Runs every minute to check for scheduled tasks
|
- **Entrypoint:** `docker-entrypoint.sh` - handles CA certs, user permissions, database init
|
||||||
- **Sync Interval**: Configured via `GITEA_MIRROR_INTERVAL` or UI (e.g., "8h", "30m", "1d")
|
- **Compose files:**
|
||||||
- **Auto-Import**: Checks GitHub for new repositories during each scheduled sync
|
- `docker-compose.alt.yml` - Quick start (pre-built image, minimal config)
|
||||||
- **Auto-Cleanup**: Removes repositories that no longer exist in GitHub (if enabled)
|
- `docker-compose.yml` - Full setup (build from source, all env vars)
|
||||||
- **Mirror Interval Update**: Updates Gitea's internal mirror interval during sync operations
|
- `docker-compose.dev.yml` - Development with hot reload
|
||||||
|
|
||||||
### Authentication Configuration
|
## Additional Resources
|
||||||
|
|
||||||
#### SSO Provider Configuration
|
- **Environment Variables:** See `docs/ENVIRONMENT_VARIABLES.md` for complete list
|
||||||
- **issuerUrl**: OIDC issuer URL (e.g., https://accounts.google.com)
|
- **Development Workflow:** See `docs/DEVELOPMENT_WORKFLOW.md`
|
||||||
- **domain**: Email domain for this provider
|
- **SSO Setup:** See `docs/SSO-OIDC-SETUP.md`
|
||||||
- **providerId**: Unique identifier for the provider
|
- **Contributing:** See `CONTRIBUTING.md` for code guidelines and scope
|
||||||
- **clientId**: OAuth client ID from provider
|
- **Graceful Shutdown:** See `docs/GRACEFUL_SHUTDOWN.md` for crash recovery details
|
||||||
- **clientSecret**: OAuth client secret from provider
|
|
||||||
- **authorizationEndpoint**: OAuth authorization URL (auto-discovered if supported)
|
|
||||||
- **tokenEndpoint**: OAuth token exchange URL (auto-discovered if supported)
|
|
||||||
- **jwksEndpoint**: JSON Web Key Set URL (optional, auto-discovered)
|
|
||||||
- **userInfoEndpoint**: User information endpoint (optional, auto-discovered)
|
|
||||||
|
|
||||||
#### OIDC Provider Settings (for external apps)
|
|
||||||
- **allowedRedirectUris**: Comma-separated list of allowed redirect URIs
|
|
||||||
- **clientId**: Generated client ID for the application
|
|
||||||
- **clientSecret**: Generated client secret for the application
|
|
||||||
- **scopes**: Available scopes (openid, profile, email)
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
- **BETTER_AUTH_SECRET**: Secret key for signing sessions (required)
|
|
||||||
- **BETTER_AUTH_URL**: Base URL for authentication (default: http://localhost:4321)
|
|
||||||
|
|
||||||
## Security Guidelines
|
|
||||||
|
|
||||||
- **Confidentiality Guidelines**:
|
|
||||||
- Dont ever say Claude Code or generated with AI anyhwere.
|
|
||||||
- Never commit without the explicict ask
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
# syntax=docker/dockerfile:1.4
|
||||||
|
|
||||||
FROM oven/bun:1.2.21-alpine AS base
|
FROM oven/bun:1.2.23-alpine AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite openssl ca-certificates
|
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite openssl ca-certificates
|
||||||
|
|
||||||
|
|||||||
17
README.md
@@ -10,10 +10,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **Upgrading to v3?** v3 requires a fresh start with a new data volume. Please read the [Upgrade Guide](UPGRADE.md) for instructions.
|
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -259,8 +255,9 @@ CLEANUP_DRY_RUN=false # Set to true to test without changes
|
|||||||
- **No Accidental Deletions**: Repository cleanup is automatically skipped if GitHub is inaccessible (account deleted, banned, or API errors)
|
- **No Accidental Deletions**: Repository cleanup is automatically skipped if GitHub is inaccessible (account deleted, banned, or API errors)
|
||||||
- **Archive Never Deletes Data**: The `archive` action preserves all repository data:
|
- **Archive Never Deletes Data**: The `archive` action preserves all repository data:
|
||||||
- Regular repositories: Made read-only using Gitea's archive feature
|
- Regular repositories: Made read-only using Gitea's archive feature
|
||||||
- Mirror repositories: Renamed with `[ARCHIVED]` prefix (Gitea API limitation prevents archiving mirrors)
|
- Mirror repositories: Renamed with `archived-` prefix (Gitea API limitation prevents archiving mirrors)
|
||||||
- Failed operations: Repository remains fully accessible even if marking as archived fails
|
- Failed operations: Repository remains fully accessible even if marking as archived fails
|
||||||
|
- **Manual Sync on Demand**: Archived mirrors stay in Gitea with automatic syncs disabled; trigger `Manual Sync` from the Repositories page whenever you need fresh data.
|
||||||
- **The Whole Point of Backups**: Your Gitea mirrors are preserved even when GitHub sources disappear - that's why you have backups!
|
- **The Whole Point of Backups**: Your Gitea mirrors are preserved even when GitHub sources disappear - that's why you have backups!
|
||||||
- **Strongly Recommended**: Always use `CLEANUP_ORPHANED_REPO_ACTION=archive` (default) instead of `delete`
|
- **Strongly Recommended**: Always use `CLEANUP_ORPHANED_REPO_ACTION=archive` (default) instead of `delete`
|
||||||
|
|
||||||
@@ -329,6 +326,8 @@ Enable users to sign in with external identity providers like Google, Azure AD,
|
|||||||
https://your-domain.com/api/auth/sso/callback/{provider-id}
|
https://your-domain.com/api/auth/sso/callback/{provider-id}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Need help? The [SSO & OIDC guide](docs/SSO-OIDC-SETUP.md) now includes a working Authentik walkthrough plus troubleshooting tips. If you upgraded from a version earlier than v3.8.10 and see `TypeError … url.startsWith` after the callback, delete the old provider and add it again using the Discover button (see [#73](https://github.com/RayLabsHQ/gitea-mirror/issues/73) and [#122](https://github.com/RayLabsHQ/gitea-mirror/issues/122)).
|
||||||
|
|
||||||
### 3. Header Authentication (Reverse Proxy)
|
### 3. Header Authentication (Reverse Proxy)
|
||||||
Perfect for automatic authentication when using reverse proxies like Authentik, Authelia, or Traefik Forward Auth.
|
Perfect for automatic authentication when using reverse proxies like Authentik, Authelia, or Traefik Forward Auth.
|
||||||
|
|
||||||
@@ -406,11 +405,11 @@ GNU General Public License v3.0 - see [LICENSE](LICENSE) file for details.
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://www.star-history.com/#RayLabsHQ/gitea-mirror&Date">
|
<a href="https://www.star-history.com/#RayLabsHQ/gitea-mirror&type=date&legend=bottom-right">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=date&theme=dark&legend=bottom-right" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=date&legend=bottom-right" />
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date" />
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=date&legend=bottom-right" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
496
Security-Audit.md
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
Date: 2025-11-07Application: Gitea Mirror v3.9.0Branch: bun-v1.3.1Scope: Full application security review
|
||||||
|
|
||||||
|
---
|
||||||
|
Executive Summary
|
||||||
|
|
||||||
|
A comprehensive security audit was conducted across all components of the Gitea Mirror application, including authentication, authorization,
|
||||||
|
encryption, input validation, API endpoints, database operations, UI components, and external integrations. The review identified 23 security
|
||||||
|
findings across multiple severity levels.
|
||||||
|
|
||||||
|
Critical Findings: 2
|
||||||
|
|
||||||
|
High Severity: 9
|
||||||
|
|
||||||
|
Medium Severity: 10
|
||||||
|
|
||||||
|
Low Severity: 2
|
||||||
|
|
||||||
|
Overall Security Posture: The application demonstrates strong foundational security practices (AES-256-GCM encryption, Drizzle ORM parameterized
|
||||||
|
queries, React auto-escaping) but has critical authorization flaws that require immediate remediation.
|
||||||
|
|
||||||
|
---
|
||||||
|
CRITICAL SEVERITY VULNERABILITIES
|
||||||
|
|
||||||
|
Vuln 1: Broken Object Level Authorization (BOLA) - Multiple Endpoints
|
||||||
|
|
||||||
|
Severity: CRITICALConfidence: 1.0Category: Authorization Bypass
|
||||||
|
|
||||||
|
Affected Files:
|
||||||
|
- src/pages/api/config/index.ts:18
|
||||||
|
- src/pages/api/job/mirror-repo.ts:16
|
||||||
|
- src/pages/api/github/repositories.ts:11
|
||||||
|
- src/pages/api/dashboard/index.ts:9
|
||||||
|
- src/pages/api/activities/index.ts:8
|
||||||
|
- src/pages/api/sync/repository.ts:15
|
||||||
|
- src/pages/api/sync/organization.ts:14
|
||||||
|
- src/pages/api/job/mirror-org.ts:14
|
||||||
|
- src/pages/api/job/retry-repo.ts:18
|
||||||
|
- src/pages/api/job/sync-repo.ts:11
|
||||||
|
- src/pages/api/job/schedule-sync-repo.ts:13
|
||||||
|
|
||||||
|
Description: Multiple API endpoints accept a userId parameter from client requests without validating that the authenticated user matches the
|
||||||
|
requested userId. This allows any authenticated user to access and manipulate any other user's data.
|
||||||
|
|
||||||
|
Exploit Scenario:
|
||||||
|
POST /api/config/index
|
||||||
|
Content-Type: application/json
|
||||||
|
Cookie: better-auth-session=attacker-session
|
||||||
|
|
||||||
|
{
|
||||||
|
"userId": "victim-user-id",
|
||||||
|
"giteaConfig": {
|
||||||
|
"token": "attacker-controlled-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result: Attacker modifies victim's configuration, steals encrypted tokens, triggers mirror operations, or accesses activity logs.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
export const POST: APIRoute = async (context) => {
|
||||||
|
// 1. Validate authentication
|
||||||
|
const { user, response } = await requireAuth(context);
|
||||||
|
if (response) return response;
|
||||||
|
|
||||||
|
// 2. Use authenticated userId from session, NOT from request body
|
||||||
|
const authenticatedUserId = user!.id;
|
||||||
|
|
||||||
|
// 3. Don't accept userId from client
|
||||||
|
const body = await request.json();
|
||||||
|
const { githubConfig, giteaConfig } = body;
|
||||||
|
|
||||||
|
// 4. Use session userId for all operations
|
||||||
|
const config = await db
|
||||||
|
.select()
|
||||||
|
.from(configs)
|
||||||
|
.where(eq(configs.userId, authenticatedUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 2: Header Authentication Spoofing - src/lib/auth-header.ts:48
|
||||||
|
|
||||||
|
Severity: CRITICALConfidence: 0.95Category: Authentication Bypass
|
||||||
|
|
||||||
|
Description: The header authentication mechanism trusts HTTP headers (X-Authentik-Username, X-Authentik-Email) without proper validation. If the
|
||||||
|
application is accessible without a properly configured reverse proxy, attackers can send these headers directly to impersonate any user.
|
||||||
|
|
||||||
|
Exploit Scenario:
|
||||||
|
GET /api/config/index?userId=admin
|
||||||
|
Host: gitea-mirror.example.com
|
||||||
|
X-Authentik-Username: admin
|
||||||
|
X-Authentik-Email: admin@example.com
|
||||||
|
|
||||||
|
Result: Attacker gains admin access without authentication.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
// 1. Add trusted proxy IP validation
|
||||||
|
const TRUSTED_PROXY_IPS = process.env.TRUSTED_PROXY_IPS?.split(',') || [];
|
||||||
|
|
||||||
|
export function extractUserFromHeaders(headers: Headers, remoteIp: string): UserInfo | null {
|
||||||
|
// Validate request comes from trusted reverse proxy
|
||||||
|
if (TRUSTED_PROXY_IPS.length > 0 && !TRUSTED_PROXY_IPS.includes(remoteIp)) {
|
||||||
|
console.warn(`Header auth rejected: untrusted IP ${remoteIp}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Add shared secret validation
|
||||||
|
const authSecret = headers.get('X-Auth-Secret');
|
||||||
|
if (authSecret !== process.env.HEADER_AUTH_SECRET) {
|
||||||
|
console.warn('Header auth rejected: invalid secret');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with header extraction...
|
||||||
|
}
|
||||||
|
|
||||||
|
Additional Requirements:
|
||||||
|
- Document that header auth MUST only be used behind a reverse proxy
|
||||||
|
- Reverse proxy MUST strip X-Authentik-* headers from untrusted requests
|
||||||
|
- Direct access MUST be blocked via firewall rules
|
||||||
|
|
||||||
|
---
|
||||||
|
HIGH SEVERITY VULNERABILITIES
|
||||||
|
|
||||||
|
Vuln 3: Missing Authentication on Critical Endpoints
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 1.0Category: Authentication Bypass
|
||||||
|
|
||||||
|
Affected Endpoints:
|
||||||
|
- /api/github/test-connection - Tests GitHub connection with user's token
|
||||||
|
- /api/gitea/test-connection - Tests Gitea connection with user's token
|
||||||
|
- /api/rate-limit/index - Exposes rate limit info
|
||||||
|
- /api/events/index - SSE endpoint for events
|
||||||
|
- /api/activities/cleanup - Deletes activities
|
||||||
|
|
||||||
|
Description: Several sensitive endpoints lack authentication checks entirely.
|
||||||
|
|
||||||
|
Exploit Scenario:
|
||||||
|
POST /api/github/test-connection
|
||||||
|
{"token": "stolen-token", "userId": "victim"}
|
||||||
|
|
||||||
|
Recommendation: Add requireAuth to all sensitive endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 4: Token Exposure Through Config API - src/pages/api/config/index.ts:220
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 0.9Category: Sensitive Data Exposure
|
||||||
|
|
||||||
|
Description: The GET /api/config/index endpoint decrypts and returns GitHub/Gitea API tokens in plaintext. Combined with BOLA, any user can retrieve
|
||||||
|
any other user's tokens.
|
||||||
|
|
||||||
|
Exploit Scenario:
|
||||||
|
1. Attacker authenticates as User A
|
||||||
|
2. Calls /api/config/index?userId=admin
|
||||||
|
3. Receives admin's decrypted GitHub Personal Access Token
|
||||||
|
4. Uses stolen token to access admin's repositories
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
// Never send full tokens - use masked versions
|
||||||
|
if (githubConfig.token) {
|
||||||
|
const decrypted = decrypt(githubConfig.token);
|
||||||
|
githubConfig.token = maskToken(decrypted); // "ghp_****...last4"
|
||||||
|
githubConfig.tokenSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 5: Static Salt in Key Derivation - src/lib/utils/encryption.ts:21
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 0.95Category: Cryptographic Weakness
|
||||||
|
|
||||||
|
Description: The key derivation function uses a static, hardcoded salt that reduces protection against precomputation attacks.
|
||||||
|
|
||||||
|
Exploit Scenario: If ENCRYPTION_SECRET is weak or compromised, the static salt makes brute-force attacks more efficient. Attackers can build targeted
|
||||||
|
rainbow tables.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
// Generate and store unique salt per installation
|
||||||
|
const SALT_FILE = '/app/data/.encryption_salt';
|
||||||
|
let installationSalt: Buffer;
|
||||||
|
|
||||||
|
if (fs.existsSync(SALT_FILE)) {
|
||||||
|
installationSalt = Buffer.from(fs.readFileSync(SALT_FILE, 'utf8'), 'hex');
|
||||||
|
} else {
|
||||||
|
installationSalt = crypto.randomBytes(32);
|
||||||
|
fs.writeFileSync(SALT_FILE, installationSalt.toString('hex'));
|
||||||
|
fs.chmodSync(SALT_FILE, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
return crypto.pbkdf2Sync(secret, installationSalt, ITERATIONS, KEY_LENGTH, 'sha256');
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 6: Weak Default Secret - src/lib/config.ts:22
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 0.90Category: Hardcoded Credentials
|
||||||
|
|
||||||
|
Description: The application uses a hardcoded default for BETTER_AUTH_SECRET when not set.
|
||||||
|
|
||||||
|
Exploit Scenario: Non-Docker deployments may use the weak default. Attackers can forge authentication tokens using the known default secret.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
BETTER_AUTH_SECRET: (() => {
|
||||||
|
const secret = process.env.BETTER_AUTH_SECRET;
|
||||||
|
const weakDefaults = ["your-secret-key-change-this-in-production"];
|
||||||
|
|
||||||
|
if (!secret || weakDefaults.includes(secret)) {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
throw new Error("BETTER_AUTH_SECRET required. Generate with: openssl rand -base64 32");
|
||||||
|
}
|
||||||
|
return "dev-secret-minimum-32-chars";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secret.length < 32) {
|
||||||
|
throw new Error("BETTER_AUTH_SECRET must be at least 32 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
})()
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 7: Unencrypted OAuth Client Secrets - src/lib/db/schema.ts:569
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 0.92Category: Sensitive Data Exposure
|
||||||
|
|
||||||
|
Description: OAuth application client secrets and SSO provider credentials are stored as plaintext, inconsistent with encrypted GitHub/Gitea tokens.
|
||||||
|
|
||||||
|
Exploit Scenario: Database leak exposes OAuth client secrets, allowing attackers to impersonate the application to external OAuth providers.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
// Encrypt before storage
|
||||||
|
import { encrypt } from "@/lib/utils/encryption";
|
||||||
|
|
||||||
|
await db.insert(oauthApplications).values({
|
||||||
|
clientSecret: encrypt(clientSecret),
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add decryption helper
|
||||||
|
export function decryptOAuthClientSecret(encrypted: string): string {
|
||||||
|
return decrypt(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 8: SSRF in OIDC Discovery - src/pages/api/sso/discover.ts:48
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 0.95Category: Server-Side Request Forgery
|
||||||
|
|
||||||
|
Description: The endpoint accepts user-provided issuer URL and fetches ${issuer}/.well-known/openid-configuration without validating against internal
|
||||||
|
networks.
|
||||||
|
|
||||||
|
Exploit Scenario:
|
||||||
|
POST /api/sso/discover
|
||||||
|
{"issuer": "http://169.254.169.254/latest/meta-data"}
|
||||||
|
|
||||||
|
Result: Access to AWS metadata service, internal APIs, or port scanning.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
// Block private IPs and localhost
|
||||||
|
const ALLOWED_PROTOCOLS = ['https:'];
|
||||||
|
const hostname = parsedIssuer.hostname.toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!ALLOWED_PROTOCOLS.includes(parsedIssuer.protocol) ||
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname.match(/^10\./) ||
|
||||||
|
hostname.match(/^192\.168\./) ||
|
||||||
|
hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./) ||
|
||||||
|
hostname.match(/^169\.254\./) ||
|
||||||
|
hostname.match(/^127\./)
|
||||||
|
) {
|
||||||
|
return new Response(JSON.stringify({ error: "Invalid issuer" }), { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 9: SSRF in Gitea Connection Test - src/pages/api/gitea/test-connection.ts:29
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 0.95Category: SSRF / Input Validation
|
||||||
|
|
||||||
|
Description: Accepts user-provided url and makes HTTP request without validation.
|
||||||
|
|
||||||
|
Exploit Scenario: Same as Vuln 8 - internal network scanning, metadata service access.
|
||||||
|
|
||||||
|
Recommendation: Apply same URL validation as Vuln 8.
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 10: Command Injection in Docker Build - scripts/build-docker.sh:72
|
||||||
|
|
||||||
|
Severity: HIGHConfidence: 0.95Category: Command Injection
|
||||||
|
|
||||||
|
Description: Uses eval with dynamically constructed Docker command including environment variables.
|
||||||
|
|
||||||
|
Exploit Scenario:
|
||||||
|
DOCKER_IMAGE='test; rm -rf /; #'
|
||||||
|
./scripts/build-docker.sh
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
# Replace eval with direct execution
|
||||||
|
if docker buildx build --platform linux/amd64,linux/arm64 \
|
||||||
|
-t "$FULL_IMAGE_NAME" \
|
||||||
|
${LOAD:+--load} \
|
||||||
|
${PUSH:+--push} \
|
||||||
|
.; then
|
||||||
|
echo "Success"
|
||||||
|
fi
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 11: Path Traversal in Gitea API - src/lib/gitea.ts:193
|
||||||
|
|
||||||
|
Severity: MEDIUM-HIGHConfidence: 0.85Category: Path Traversal / SSRF
|
||||||
|
|
||||||
|
Description: Repository owner/name interpolated into URLs without encoding.
|
||||||
|
|
||||||
|
Exploit Scenario:
|
||||||
|
owner = "../../admin"
|
||||||
|
repoName = "users/../../../config"
|
||||||
|
// Results in: /api/v1/repos/../../admin/users/../../../config
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.url}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`,
|
||||||
|
// ...
|
||||||
|
);
|
||||||
|
|
||||||
|
Apply to all URL constructions in gitea.ts and gitea-enhanced.ts.
|
||||||
|
|
||||||
|
---
|
||||||
|
MEDIUM SEVERITY VULNERABILITIES
|
||||||
|
|
||||||
|
Vuln 12: Weak Session Configuration - src/lib/auth.ts:105
|
||||||
|
|
||||||
|
Severity: MEDIUMConfidence: 0.85
|
||||||
|
|
||||||
|
Description: 30-day session expiration is excessive; missing explicit security flags.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
session: {
|
||||||
|
expiresIn: 60 * 60 * 24 * 7, // 7 days max
|
||||||
|
cookieOptions: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
httpOnly: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 13: Missing CSRF Protection
|
||||||
|
|
||||||
|
Severity: MEDIUMConfidence: 0.80
|
||||||
|
|
||||||
|
Description: No visible CSRF token validation on state-changing operations.
|
||||||
|
|
||||||
|
Recommendation: Verify Better Auth CSRF protection is enabled; implement explicit tokens if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 14: Weak Random Number Generation - src/lib/utils.ts:12
|
||||||
|
|
||||||
|
Severity: MEDIUMConfidence: 0.85
|
||||||
|
|
||||||
|
Description: generateRandomString() uses Math.random() (not cryptographically secure) for OAuth client credentials.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
// Use existing secure function
|
||||||
|
const clientId = `client_${generateSecureToken(16)}`;
|
||||||
|
const clientSecret = `secret_${generateSecureToken(24)}`;
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 15-18: Input Validation Issues
|
||||||
|
|
||||||
|
Severity: MEDIUMConfidence: 0.85
|
||||||
|
|
||||||
|
Affected:
|
||||||
|
- src/pages/api/repositories/[id].ts:24 - destinationOrg not validated
|
||||||
|
- src/pages/api/organizations/[id].ts:24 - destinationOrg not validated
|
||||||
|
- src/pages/api/job/mirror-repo.ts:19 - repositoryIds array not validated
|
||||||
|
- src/lib/helpers.ts:49 - Repository names in messages not sanitized
|
||||||
|
|
||||||
|
Recommendation: Use Zod schemas for all user inputs:
|
||||||
|
const updateRepoSchema = z.object({
|
||||||
|
destinationOrg: z.string()
|
||||||
|
.min(1)
|
||||||
|
.max(100)
|
||||||
|
.regex(/^[a-zA-Z0-9\-_\.]+$/)
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
---
|
||||||
|
LOW SEVERITY VULNERABILITIES
|
||||||
|
|
||||||
|
Vuln 19: XSS via Astro set:html - src/pages/docs/quickstart.astro:102
|
||||||
|
|
||||||
|
Severity: LOWConfidence: 0.85
|
||||||
|
|
||||||
|
Description: Uses set:html with static data; risky pattern if copied with dynamic content.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
<p class="text-sm" set:text={item.text}></p>
|
||||||
|
|
||||||
|
---
|
||||||
|
Vuln 20: Open Redirect in OAuth - src/components/oauth/ConsentPage.tsx:118
|
||||||
|
|
||||||
|
Severity: LOWConfidence: 0.90
|
||||||
|
|
||||||
|
Description: OAuth redirect uses window.location.href without explicit protocol validation.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||||
|
throw new Error('Invalid protocol');
|
||||||
|
}
|
||||||
|
window.location.assign(url.toString());
|
||||||
|
|
||||||
|
---
|
||||||
|
POSITIVE SECURITY FINDINGS ✅
|
||||||
|
|
||||||
|
The codebase demonstrates excellent security practices:
|
||||||
|
|
||||||
|
1. SQL Injection Protection - Drizzle ORM with parameterized queries throughout
|
||||||
|
2. Token Encryption - AES-256-GCM for GitHub/Gitea tokens
|
||||||
|
3. XSS Protection - React auto-escaping, no dangerouslySetInnerHTML found
|
||||||
|
4. Password Hashing - bcrypt via Better Auth
|
||||||
|
5. Secure Random - crypto.randomBytes() in encryption functions
|
||||||
|
6. Input Validation - Comprehensive Zod schemas
|
||||||
|
7. Safe Path Operations - path.join() with proper base directories
|
||||||
|
8. No Command Execution - TypeScript codebase doesn't use child_process
|
||||||
|
9. Authentication Framework - Better Auth with session management
|
||||||
|
10. Type Safety - TypeScript strict mode
|
||||||
|
|
||||||
|
---
|
||||||
|
REMEDIATION PRIORITY
|
||||||
|
|
||||||
|
Phase 1: EMERGENCY (Deploy Today)
|
||||||
|
|
||||||
|
1. Fix BOLA vulnerabilities - Add authentication and userId validation (Vuln 1)
|
||||||
|
2. Disable or secure header auth - Add IP whitelist validation (Vuln 2)
|
||||||
|
3. Add authentication checks - Protect unprotected endpoints (Vuln 3)
|
||||||
|
|
||||||
|
Phase 2: URGENT (This Week)
|
||||||
|
|
||||||
|
1. Implement token masking - Don't return decrypted tokens (Vuln 4)
|
||||||
|
2. Fix SSRF vulnerabilities - Add URL validation (Vuln 8, 9)
|
||||||
|
3. Fix command injection - Remove eval from build script (Vuln 10)
|
||||||
|
4. Add URL encoding - Path traversal fix (Vuln 11)
|
||||||
|
|
||||||
|
Phase 3: HIGH (This Sprint)
|
||||||
|
|
||||||
|
1. Fix KDF salt - Generate unique salt per installation (Vuln 5)
|
||||||
|
2. Enforce strong secrets - Fail if weak defaults used (Vuln 6)
|
||||||
|
3. Encrypt OAuth secrets - Match GitHub/Gitea token encryption (Vuln 7)
|
||||||
|
4. Reduce session expiration - 7 days maximum (Vuln 12)
|
||||||
|
5. Add CSRF protection - Explicit tokens (Vuln 13)
|
||||||
|
|
||||||
|
Phase 4: MEDIUM (Next Sprint)
|
||||||
|
|
||||||
|
1. Input validation - Zod schemas for all endpoints (Vuln 14-18)
|
||||||
|
2. Fix weak RNG - Use crypto.randomBytes (Vuln 14)
|
||||||
|
|
||||||
|
Phase 5: LOW (Backlog)
|
||||||
|
|
||||||
|
1. Remove set:html - Use safer alternatives (Vuln 19)
|
||||||
|
2. OAuth redirect validation - Add protocol check (Vuln 20)
|
||||||
|
|
||||||
|
---
|
||||||
|
TESTING RECOMMENDATIONS
|
||||||
|
|
||||||
|
Proof of Concept Tests
|
||||||
|
|
||||||
|
# Test BOLA
|
||||||
|
curl -X POST http://localhost:4321/api/config/index \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: better-auth-session=<attacker-session>" \
|
||||||
|
-d '{"userId":"victim-id","githubConfig":{...}}'
|
||||||
|
|
||||||
|
# Test header spoofing
|
||||||
|
curl http://localhost:4321/api/config/index?userId=admin \
|
||||||
|
-H "X-Authentik-Username: admin" \
|
||||||
|
-H "X-Authentik-Email: admin@example.com"
|
||||||
|
|
||||||
|
# Test SSRF
|
||||||
|
curl -X POST http://localhost:4321/api/sso/discover \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"issuer":"http://169.254.169.254/latest/meta-data"}'
|
||||||
|
|
||||||
|
---
|
||||||
|
SECURITY GRADE
|
||||||
|
|
||||||
|
Overall Security Grade: C+
|
||||||
|
- Would be B+ after Phase 1-2 fixes
|
||||||
|
- Would be A- after all fixes
|
||||||
|
|
||||||
|
Critical Issues: 2 (authorization bypass, auth spoofing)Risk Level: HIGH due to BOLA allowing complete account takeover
|
||||||
|
|
||||||
|
---
|
||||||
|
CONCLUSION
|
||||||
|
|
||||||
|
The Gitea Mirror application has a strong security foundation with proper encryption, ORM usage, and XSS protection. However, critical authorization
|
||||||
|
flaws in API endpoints allow authenticated users to access any other user's data. These must be fixed immediately before production deployment.
|
||||||
|
|
||||||
|
The development team has clearly prioritized security (encryption, input validation, type safety), and the identified issues are fixable with
|
||||||
|
moderate effort. After remediation, this will be a secure, production-ready application.
|
||||||
74
UPGRADE.md
@@ -1,74 +0,0 @@
|
|||||||
# Upgrade Guide
|
|
||||||
|
|
||||||
## Upgrading to v3.0
|
|
||||||
|
|
||||||
> **⚠️ IMPORTANT**: v3.0 requires a fresh start. There is no automated migration from v2.x to v3.0.
|
|
||||||
|
|
||||||
### Why No Migration?
|
|
||||||
|
|
||||||
v3.0 introduces fundamental changes to the application architecture:
|
|
||||||
- **Authentication**: Switched from JWT to Better Auth
|
|
||||||
- **Database**: Now uses Drizzle ORM with proper migrations
|
|
||||||
- **Security**: All tokens are now encrypted
|
|
||||||
- **Features**: Added SSO support and OIDC provider functionality
|
|
||||||
|
|
||||||
Due to these extensive changes, we recommend starting fresh with v3.0 for the best experience.
|
|
||||||
|
|
||||||
### Upgrade Steps
|
|
||||||
|
|
||||||
1. **Stop your v2.x container**
|
|
||||||
```bash
|
|
||||||
docker stop gitea-mirror
|
|
||||||
docker rm gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Backup your v2.x data (optional)**
|
|
||||||
```bash
|
|
||||||
# If you want to keep your v2 data for reference
|
|
||||||
docker run --rm -v gitea-mirror-data:/data -v $(pwd):/backup alpine tar czf /backup/gitea-mirror-v2-backup.tar.gz -C /data .
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Create a new volume for v3**
|
|
||||||
```bash
|
|
||||||
docker volume create gitea-mirror-v3-data
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Run v3 with the new volume**
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name gitea-mirror \
|
|
||||||
-p 4321:4321 \
|
|
||||||
-v gitea-mirror-v3-data:/app/data \
|
|
||||||
-e BETTER_AUTH_SECRET=your-secret-key \
|
|
||||||
-e ENCRYPTION_SECRET=your-encryption-key \
|
|
||||||
arunavo4/gitea-mirror:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Set up your configuration again**
|
|
||||||
- Navigate to http://localhost:4321
|
|
||||||
- Create a new admin account
|
|
||||||
- Re-enter your GitHub and Gitea credentials
|
|
||||||
- Configure your mirror settings
|
|
||||||
|
|
||||||
### What Happens to My Existing Mirrors?
|
|
||||||
|
|
||||||
Your existing mirrors in Gitea are **not affected**. The application will:
|
|
||||||
- Recognize existing repositories when you re-import
|
|
||||||
- Skip creating duplicates
|
|
||||||
- Resume normal mirror operations
|
|
||||||
|
|
||||||
### Environment Variable Changes
|
|
||||||
|
|
||||||
v3.0 uses different environment variables:
|
|
||||||
|
|
||||||
| v2.x | v3.0 | Notes |
|
|
||||||
|------|------|-------|
|
|
||||||
| `JWT_SECRET` | `BETTER_AUTH_SECRET` | Required for session management |
|
|
||||||
| - | `ENCRYPTION_SECRET` | New - required for token encryption |
|
|
||||||
|
|
||||||
### Need Help?
|
|
||||||
|
|
||||||
If you have questions about upgrading:
|
|
||||||
1. Check the [README](README.md) for v3 setup instructions
|
|
||||||
2. Review your v2 configuration before upgrading
|
|
||||||
3. Open an issue if you encounter problems
|
|
||||||
@@ -14,9 +14,9 @@ export default defineConfig({
|
|||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['bun']
|
external: ['bun', 'bun:*'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
integrations: [react()]
|
integrations: [react()]
|
||||||
});
|
});
|
||||||
|
|||||||
381
bun.lock
@@ -4,12 +4,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.5",
|
||||||
"@astrojs/mdx": "4.3.5",
|
"@astrojs/mdx": "4.3.7",
|
||||||
"@astrojs/node": "9.4.3",
|
"@astrojs/node": "9.5.0",
|
||||||
"@astrojs/react": "^4.3.1",
|
"@astrojs/react": "^4.4.0",
|
||||||
"@better-auth/sso": "^1.3.9",
|
"@better-auth/sso": "1.4.0-beta.12",
|
||||||
"@octokit/plugin-throttling": "^11.0.1",
|
"@octokit/plugin-throttling": "^11.0.2",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
@@ -29,47 +29,48 @@
|
|||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.15",
|
||||||
"@tanstack/react-virtual": "^3.13.12",
|
"@tanstack/react-virtual": "^3.13.12",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.2.2",
|
||||||
"astro": "^5.13.7",
|
"astro": "^5.14.8",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"better-auth": "^1.3.9",
|
"better-auth": "1.4.0-beta.13",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.6",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.546.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.15",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.5",
|
"zod": "^4.1.12",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "^1.2.21",
|
"@types/bun": "^1.3.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.5",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.6",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -85,23 +86,23 @@
|
|||||||
|
|
||||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
||||||
|
|
||||||
"@astrojs/check": ["@astrojs/check@0.9.4", "", { "dependencies": { "@astrojs/language-server": "^2.15.0", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "dist/bin.js" } }, "sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA=="],
|
"@astrojs/check": ["@astrojs/check@0.9.5", "", { "dependencies": { "@astrojs/language-server": "^2.15.0", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "dist/bin.js" } }, "sha512-88vc8n2eJ1Oua74yXSGo/8ABMeypfQPGEzuoAx2awL9Ju8cE6tZ2Rz9jVx5hIExHK5gKVhpxfZj4WXm7e32g1w=="],
|
||||||
|
|
||||||
"@astrojs/compiler": ["@astrojs/compiler@2.12.2", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="],
|
"@astrojs/compiler": ["@astrojs/compiler@2.12.2", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="],
|
||||||
|
|
||||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.2", "", {}, "sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g=="],
|
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.4", "", {}, "sha512-lDA9MqE8WGi7T/t2BMi+EAXhs4Vcvr94Gqx3q15cFEz8oFZMO4/SFBqYr/UcmNlvW+35alowkVj+w9VhLvs5Cw=="],
|
||||||
|
|
||||||
"@astrojs/language-server": ["@astrojs/language-server@2.15.4", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", "@volar/kit": "~2.4.7", "@volar/language-core": "~2.4.7", "@volar/language-server": "~2.4.7", "@volar/language-service": "~2.4.7", "fast-glob": "^3.2.12", "muggle-string": "^0.4.1", "volar-service-css": "0.0.62", "volar-service-emmet": "0.0.62", "volar-service-html": "0.0.62", "volar-service-prettier": "0.0.62", "volar-service-typescript": "0.0.62", "volar-service-typescript-twoslash-queries": "0.0.62", "volar-service-yaml": "0.0.62", "vscode-html-languageservice": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A=="],
|
"@astrojs/language-server": ["@astrojs/language-server@2.15.4", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", "@volar/kit": "~2.4.7", "@volar/language-core": "~2.4.7", "@volar/language-server": "~2.4.7", "@volar/language-service": "~2.4.7", "fast-glob": "^3.2.12", "muggle-string": "^0.4.1", "volar-service-css": "0.0.62", "volar-service-emmet": "0.0.62", "volar-service-html": "0.0.62", "volar-service-prettier": "0.0.62", "volar-service-typescript": "0.0.62", "volar-service-typescript-twoslash-queries": "0.0.62", "volar-service-yaml": "0.0.62", "vscode-html-languageservice": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.6", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.2", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.4", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bwylYktCTsLMVoCOEHbn2GSUA3c5KT/qilekBKA3CBng0bo1TYjNZPr761vxumRk9kJGqTOtU+fgCAp5Vwokug=="],
|
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.8", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.4", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.13.0", "smol-toml": "^1.4.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-uFNyFWadnULWK2cOw4n0hLKeu+xaVWeuECdP10cQ3K2fkybtTlhb7J7TcScdjmS8Yps7oje9S/ehYMfZrhrgCg=="],
|
||||||
|
|
||||||
"@astrojs/mdx": ["@astrojs/mdx@4.3.5", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.6", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-YB3Hhsvl1BxyY0ARe1OrnVzLNKDPXAz9epYvmL+MQ8A85duSsSLQaO3GHB6/qZJKNoLmP6PptOtCONCKkbhPeQ=="],
|
"@astrojs/mdx": ["@astrojs/mdx@4.3.7", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.8", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-5SRmvMyT/UMWaU2eoD+htnXtE2mUZZEH2K/nEzhuEy+iCsOSuS/DUry59WuKUJRQETi1mgJFdNR4dZLJHYVuRA=="],
|
||||||
|
|
||||||
"@astrojs/node": ["@astrojs/node@9.4.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.2", "send": "^1.2.0", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^5.7.0" } }, "sha512-P9BQHY8wQU1y9obknXzxV5SS3EpdaRnuDuHKr3RFC7t+2AzcMXeVmMJprQGijnQ8VdijJ8aS7+12tx325TURMQ=="],
|
"@astrojs/node": ["@astrojs/node@9.5.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.4", "send": "^1.2.0", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^5.14.3" } }, "sha512-x1whLIatmCefaqJA8FjfI+P6FStF+bqmmrib0OUGM1M3cZhAXKLgPx6UF2AzQ3JgpXgCWYM24MHtraPvZhhyLQ=="],
|
||||||
|
|
||||||
"@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
"@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||||
|
|
||||||
"@astrojs/react": ["@astrojs/react@4.3.1", "", { "dependencies": { "@vitejs/plugin-react": "^4.7.0", "ultrahtml": "^1.6.0", "vite": "^6.3.6" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, "sha512-Jhv35TsDHuQLvwof2z10P3g1s9wIR4UN9jE7O4NZBJNXOt/+qk+L0rY9th4SX7VzccKmRltUGxAhI1cXH52gXw=="],
|
"@astrojs/react": ["@astrojs/react@4.4.0", "", { "dependencies": { "@vitejs/plugin-react": "^4.7.0", "ultrahtml": "^1.6.0", "vite": "^6.3.6" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, "sha512-RzblkVImAFdV1C0AWsSWzS70Z0FMtW2p0XXkNYu3QePfyVJta3JIy8m8jY8271etaCZtpFjsE2UaiHGZIBm6nw=="],
|
||||||
|
|
||||||
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
||||||
|
|
||||||
"@babel/core": ["@babel/core@7.28.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ=="],
|
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||||
|
|
||||||
@@ -133,9 +134,9 @@
|
|||||||
|
|
||||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw=="],
|
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
@@ -145,17 +146,21 @@
|
|||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||||
|
|
||||||
"@better-auth/sso": ["@better-auth/sso@1.3.9", "", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "fast-xml-parser": "^5.2.5", "jose": "^5.10.0", "oauth2-mock-server": "^7.2.1", "samlify": "^2.10.1", "zod": "^4.1.5" }, "peerDependencies": { "better-auth": "1.3.9" } }, "sha512-rYbFtl/MpD6iEyKSnwTlG8jdu6xqYmDF1Bmx2P8NaXzTtkhb432Q045vqB7cWpZKpLCf2tNK5QeAyPhS0zg+Gw=="],
|
"@better-auth/core": ["@better-auth/core@1.4.0-beta.13", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.0.24", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-EGySsNv6HQYnlRQDIa7otIMrwFoC0gGLxBum9lC6C3wAsF4l4pn/ECcdIriFpc9ewLb8mGkeMSpvjVBUBND6ew=="],
|
||||||
|
|
||||||
"@better-auth/utils": ["@better-auth/utils@0.2.6", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA=="],
|
"@better-auth/sso": ["@better-auth/sso@1.4.0-beta.12", "", { "dependencies": { "@better-fetch/fetch": "1.1.18", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "oauth2-mock-server": "^7.2.1", "samlify": "^2.10.1", "zod": "^4.1.5" }, "peerDependencies": { "better-auth": "1.4.0-beta.12" } }, "sha512-iuRuy59J3yXQihZJ34rqYClWyuVjSkxuBkdFblccKbOhNy7pmRO1lfmBMpyeth3ET5Cp0PDVV/z1XBbDcQp0LA=="],
|
||||||
|
|
||||||
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.0-beta.13", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18" }, "peerDependencies": { "@better-auth/core": "1.4.0-beta.13" } }, "sha512-910f+APALhhD79TiujzXp85Pnd2M3TlcTgBfiYF+mk3ouIkBJkl2N6D2ElcgwfiNTg50cFuTkP3AFPYioz8Arw=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||||
|
|
||||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
|
||||||
|
|
||||||
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
|
"@capsizecss/unpack": ["@capsizecss/unpack@3.0.0", "", { "dependencies": { "fontkit": "^2.0.2" } }, "sha512-+ntATQe1AlL7nTOYjwjj6w3299CgRot48wL761TUGYpYgAou3AaONZazp0PKZyCyWhudWsjhq1nvRHOvbMzhTA=="],
|
||||||
|
|
||||||
"@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="],
|
"@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="],
|
||||||
|
|
||||||
@@ -185,7 +190,7 @@
|
|||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
||||||
|
|
||||||
"@esbuild-kit/esm-loader": ["tsx@4.20.5", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="],
|
"@esbuild-kit/esm-loader": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
|
||||||
|
|
||||||
@@ -293,9 +298,7 @@
|
|||||||
|
|
||||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="],
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="],
|
||||||
|
|
||||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
@@ -305,7 +308,7 @@
|
|||||||
|
|
||||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||||
|
|
||||||
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
||||||
|
|
||||||
@@ -329,7 +332,7 @@
|
|||||||
|
|
||||||
"@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
|
"@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
|
||||||
|
|
||||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
"@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||||
|
|
||||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.0.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-m1KvHlueScy4mQJWvFDCxFBTIdXS0K1SgFGLmqHyX90mZdCIv6gWBbKRhatxRjhGlONuTK/hztYdaqrTXcFZdQ=="],
|
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.0.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-m1KvHlueScy4mQJWvFDCxFBTIdXS0K1SgFGLmqHyX90mZdCIv6gWBbKRhatxRjhGlONuTK/hztYdaqrTXcFZdQ=="],
|
||||||
|
|
||||||
@@ -337,7 +340,7 @@
|
|||||||
|
|
||||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
|
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
|
||||||
|
|
||||||
"@octokit/plugin-throttling": ["@octokit/plugin-throttling@11.0.1", "", { "dependencies": { "@octokit/types": "^14.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^7.0.0" } }, "sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw=="],
|
"@octokit/plugin-throttling": ["@octokit/plugin-throttling@11.0.2", "", { "dependencies": { "@octokit/types": "^15.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^7.0.0" } }, "sha512-ntNIig4zZhQVOZF4fG9Wt8QCoz9ehb+xnlUwp74Ic2ANChCk8oKmRwV9zDDCtrvU1aERIOvtng8wsalEX7Jk5Q=="],
|
||||||
|
|
||||||
"@octokit/request": ["@octokit/request@10.0.2", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA=="],
|
"@octokit/request": ["@octokit/request@10.0.2", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA=="],
|
||||||
|
|
||||||
@@ -345,7 +348,7 @@
|
|||||||
|
|
||||||
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||||
|
|
||||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
"@octokit/types": ["@octokit/types@15.0.0", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ=="],
|
||||||
|
|
||||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||||
|
|
||||||
@@ -451,7 +454,7 @@
|
|||||||
|
|
||||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.34", "", {}, "sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="],
|
||||||
|
|
||||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||||
|
|
||||||
@@ -515,35 +518,35 @@
|
|||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.15", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.15" } }, "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.15", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.15", "@tailwindcss/oxide-darwin-arm64": "4.1.15", "@tailwindcss/oxide-darwin-x64": "4.1.15", "@tailwindcss/oxide-freebsd-x64": "4.1.15", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.15", "@tailwindcss/oxide-linux-arm64-musl": "4.1.15", "@tailwindcss/oxide-linux-x64-gnu": "4.1.15", "@tailwindcss/oxide-linux-x64-musl": "4.1.15", "@tailwindcss/oxide-wasm32-wasi": "4.1.15", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.15", "@tailwindcss/oxide-win32-x64-msvc": "4.1.15" } }, "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.15", "", { "os": "android", "cpu": "arm64" }, "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="],
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.15", "", { "os": "linux", "cpu": "arm" }, "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="],
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="],
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="],
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.15", "", { "os": "linux", "cpu": "x64" }, "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="],
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.15", "", { "os": "linux", "cpu": "x64" }, "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="],
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.15", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.15", "", { "os": "win32", "cpu": "x64" }, "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w=="],
|
||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="],
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.15", "", { "dependencies": { "@tailwindcss/node": "4.1.15", "@tailwindcss/oxide": "4.1.15", "tailwindcss": "4.1.15" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA=="],
|
||||||
|
|
||||||
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="],
|
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="],
|
||||||
|
|
||||||
@@ -551,7 +554,7 @@
|
|||||||
|
|
||||||
"@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="],
|
"@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="],
|
||||||
|
|
||||||
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.8.0", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ=="],
|
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
|
||||||
|
|
||||||
"@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="],
|
"@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="],
|
||||||
|
|
||||||
@@ -567,7 +570,7 @@
|
|||||||
|
|
||||||
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
|
||||||
|
|
||||||
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
||||||
|
|
||||||
@@ -597,9 +600,9 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
|
"@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
@@ -607,7 +610,7 @@
|
|||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.2", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.34", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.4", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.38", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||||
|
|
||||||
@@ -679,7 +682,7 @@
|
|||||||
|
|
||||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||||
|
|
||||||
"astro": ["astro@5.13.7", "", { "dependencies": { "@astrojs/compiler": "^2.12.2", "@astrojs/internal-helpers": "0.7.2", "@astrojs/markdown-remark": "6.3.6", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.2.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.1", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.18", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.3.0", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.2", "shiki": "^3.12.0", "smol-toml": "^1.4.2", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.5.2", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.0", "vfile": "^6.0.3", "vite": "^6.3.6", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-Of2tST7ErbE4y1dVb4aWDXaQSIRBAfraJ4jDqaA3PzPRJOn6Ina36+tQ+8BezjYqiWwRRJdOEE07PRAJXnsddw=="],
|
"astro": ["astro@5.14.8", "", { "dependencies": { "@astrojs/compiler": "^2.12.2", "@astrojs/internal-helpers": "0.7.4", "@astrojs/markdown-remark": "6.3.8", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^3.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.2.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.1", "deterministic-object-hash": "^2.0.2", "devalue": "^5.3.2", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.18", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.3.0", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.2", "shiki": "^3.12.0", "smol-toml": "^1.4.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.6.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.0", "vfile": "^6.0.3", "vite": "^6.3.6", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-nKqCLs7BFvGQL9QWQOUqxHhlHtV0UMLXz1ANJygozvjcexBWS7FYkWI2LzRPMNYmbW4msIWNWnX2RvLdvI5Cnw=="],
|
||||||
|
|
||||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
@@ -695,11 +698,15 @@
|
|||||||
|
|
||||||
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||||
|
|
||||||
"better-auth": ["better-auth@1.3.9", "", { "dependencies": { "@better-auth/utils": "0.2.6", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.18", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^0.11.4", "zod": "^4.1.5" }, "peerDependencies": { "@lynx-js/react": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@lynx-js/react", "react", "react-dom"] }, "sha512-Ty6BHzuShlqSs7I4RMlBRQ3duOWNB7WWriIu2FJVGjQAOtTVvamzFCR4/j5ROFLoNkpvNTRF7BJozsrMICL1gw=="],
|
"better-auth": ["better-auth@1.4.0-beta.13", "", { "dependencies": { "@better-auth/core": "1.4.0-beta.13", "@better-auth/telemetry": "1.4.0-beta.13", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.24", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-VOzbsCldupk2AdNfzDmpCVajX83nwITX8S9I8TdEUURgr3kB/CDVrsN6S8t0AClMnGgB4XaeKiXUNN30CCG4aA=="],
|
||||||
|
|
||||||
"better-call": ["better-call@1.0.18", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-Ojyck3P3fs/egBmCW50tvfbCJorNV5KphfPOKrkCxPfOr8Brth1ruDtAJuhHVHEUiWrXv+vpEgWQk7m7FzhbbQ=="],
|
"better-call": ["better-call@1.0.24", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-iGqL29cstPp4xLD2MjKL1EmyAqQHjYS+cBMt4W27rPs3vf+kuqkVPA0NYaf7JciBOzVsJdNj4cbZWXC5TardWQ=="],
|
||||||
|
|
||||||
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
|
"better-sqlite3": ["better-sqlite3@12.4.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ=="],
|
||||||
|
|
||||||
|
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||||
|
|
||||||
|
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
||||||
|
|
||||||
@@ -713,9 +720,11 @@
|
|||||||
|
|
||||||
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
|
|
||||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
@@ -749,7 +758,7 @@
|
|||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||||
|
|
||||||
"ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="],
|
"ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="],
|
||||||
|
|
||||||
@@ -793,8 +802,6 @@
|
|||||||
|
|
||||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||||
|
|
||||||
"cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
|
|
||||||
|
|
||||||
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||||
|
|
||||||
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||||
@@ -815,8 +822,12 @@
|
|||||||
|
|
||||||
"decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="],
|
"decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="],
|
||||||
|
|
||||||
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||||
|
|
||||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||||
|
|
||||||
|
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||||
|
|
||||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
@@ -845,11 +856,11 @@
|
|||||||
|
|
||||||
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
|
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="],
|
"drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.44.5", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ=="],
|
"drizzle-orm": ["drizzle-orm@0.44.6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="],
|
||||||
|
|
||||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||||
|
|
||||||
@@ -867,6 +878,8 @@
|
|||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||||
|
|
||||||
"entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
|
"entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
|
||||||
@@ -911,6 +924,8 @@
|
|||||||
|
|
||||||
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||||
|
|
||||||
|
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||||
|
|
||||||
"expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="],
|
"expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="],
|
||||||
|
|
||||||
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
||||||
@@ -931,6 +946,8 @@
|
|||||||
|
|
||||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
||||||
|
|
||||||
|
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
|
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
|
||||||
@@ -945,6 +962,8 @@
|
|||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
@@ -965,6 +984,8 @@
|
|||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||||
|
|
||||||
|
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
@@ -1023,12 +1044,16 @@
|
|||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||||
|
|
||||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||||
|
|
||||||
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
@@ -1067,7 +1092,7 @@
|
|||||||
|
|
||||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
@@ -1095,6 +1120,8 @@
|
|||||||
|
|
||||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||||
|
|
||||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||||
@@ -1137,7 +1164,7 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
|
"lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
|
||||||
|
|
||||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||||
|
|
||||||
@@ -1273,13 +1300,13 @@
|
|||||||
|
|
||||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||||
|
|
||||||
|
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||||
|
|
||||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||||
|
|
||||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||||
|
|
||||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
|
||||||
|
|
||||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||||
|
|
||||||
@@ -1289,7 +1316,9 @@
|
|||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
|
"nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="],
|
||||||
|
|
||||||
|
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||||
|
|
||||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
@@ -1299,7 +1328,7 @@
|
|||||||
|
|
||||||
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||||
|
|
||||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
"node-abi": ["node-abi@3.78.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ=="],
|
||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
@@ -1327,6 +1356,8 @@
|
|||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||||
|
|
||||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
|
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
|
||||||
@@ -1363,6 +1394,8 @@
|
|||||||
|
|
||||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||||
|
|
||||||
|
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||||
|
|
||||||
"prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
|
"prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
|
||||||
|
|
||||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||||
@@ -1375,6 +1408,8 @@
|
|||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||||
@@ -1391,9 +1426,11 @@
|
|||||||
|
|
||||||
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
||||||
|
|
||||||
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
|
||||||
|
|
||||||
"react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
|
"react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
|
||||||
|
|
||||||
@@ -1407,6 +1444,8 @@
|
|||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||||
@@ -1483,7 +1522,7 @@
|
|||||||
|
|
||||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
@@ -1511,6 +1550,10 @@
|
|||||||
|
|
||||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||||
|
|
||||||
|
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||||
|
|
||||||
|
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||||
|
|
||||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||||
|
|
||||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||||
@@ -1533,12 +1576,16 @@
|
|||||||
|
|
||||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||||
|
|
||||||
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
||||||
|
|
||||||
"strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
|
"strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
|
||||||
@@ -1553,11 +1600,13 @@
|
|||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="],
|
"tailwindcss": ["tailwindcss@4.1.15", "", {}, "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||||
|
|
||||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||||
|
|
||||||
|
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||||
|
|
||||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||||
|
|
||||||
@@ -1593,9 +1642,11 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tsx": ["tsx@4.20.5", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="],
|
"tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="],
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||||
|
|
||||||
@@ -1603,7 +1654,7 @@
|
|||||||
|
|
||||||
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
|
"typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
|
||||||
|
|
||||||
@@ -1621,7 +1672,7 @@
|
|||||||
|
|
||||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||||
|
|
||||||
"unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="],
|
"unifont": ["unifont@0.6.0", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-5Fx50fFQMQL5aeHyWnZX9122sSLckcDvcfFiBf3QYeHa7a1MKJooUy52b67moi2MJYkrfo/TWY+CoLdr/w0tTA=="],
|
||||||
|
|
||||||
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
||||||
|
|
||||||
@@ -1657,6 +1708,8 @@
|
|||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||||
|
|
||||||
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
||||||
@@ -1671,7 +1724,7 @@
|
|||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
||||||
|
|
||||||
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
"vite": ["vite@6.3.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA=="],
|
||||||
|
|
||||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||||
|
|
||||||
@@ -1733,6 +1786,8 @@
|
|||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
|
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
"ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
|
"ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
|
||||||
|
|
||||||
"xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="],
|
"xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="],
|
||||||
@@ -1751,7 +1806,7 @@
|
|||||||
|
|
||||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
||||||
|
|
||||||
@@ -1767,7 +1822,7 @@
|
|||||||
|
|
||||||
"yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
|
"yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
|
||||||
|
|
||||||
"zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="],
|
"zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
|
||||||
|
|
||||||
@@ -1775,23 +1830,21 @@
|
|||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
|
"@ampproject/remapping/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="],
|
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="],
|
"@astrojs/markdown-remark/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
|
||||||
|
|
||||||
"@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
"@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||||
|
|
||||||
"@astrojs/react/vite": ["vite@6.3.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA=="],
|
|
||||||
|
|
||||||
"@astrojs/telemetry/ci-info": ["ci-info@4.2.0", "", {}, "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg=="],
|
"@astrojs/telemetry/ci-info": ["ci-info@4.2.0", "", {}, "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg=="],
|
||||||
|
|
||||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
"@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
"@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
@@ -1801,31 +1854,45 @@
|
|||||||
|
|
||||||
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
||||||
|
|
||||||
"@babel/template/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
"@babel/template/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
"@babel/template/@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
"@babel/template/@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
||||||
|
|
||||||
"@jridgewell/remapping/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
"@octokit/core/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
"@octokit/endpoint/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
|
"@octokit/graphql/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
|
"@octokit/request/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
|
"@octokit/request-error/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
"@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
"@tailwindcss/node/magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
"@tailwindcss/node/magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
@@ -1853,13 +1920,13 @@
|
|||||||
|
|
||||||
"astro/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"astro/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
"astro/vite": ["vite@6.3.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA=="],
|
"astro/tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||||
|
|
||||||
"astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||||
|
|
||||||
"better-auth/jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
|
|
||||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
@@ -1901,7 +1968,7 @@
|
|||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
"oauth2-mock-server/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||||
|
|
||||||
"ofetch/node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="],
|
"ofetch/node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="],
|
||||||
|
|
||||||
@@ -1925,6 +1992,12 @@
|
|||||||
|
|
||||||
"vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
"vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
||||||
|
|
||||||
|
"vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"vite-node/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||||
|
|
||||||
|
"vitest/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||||
|
|
||||||
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||||
|
|
||||||
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
@@ -1943,28 +2016,64 @@
|
|||||||
|
|
||||||
"yaml-language-server/yaml": ["yaml@2.2.2", "", {}, "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA=="],
|
"yaml-language-server/yaml": ["yaml@2.2.2", "", {}, "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
|
"@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ=="],
|
"@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q=="],
|
"@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA=="],
|
"@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg=="],
|
"@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
|
"@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core": ["@babel/core@7.28.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ=="],
|
||||||
|
|
||||||
"@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
"@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||||
|
|
||||||
"@astrojs/react/vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.3", "", { "dependencies": { "@babel/parser": "^7.27.3", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q=="],
|
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.3", "", { "dependencies": { "@babel/parser": "^7.27.3", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
|
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
|
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
|
"@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
|
"@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
|
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||||
|
|
||||||
"@tailwindcss/node/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
"@tailwindcss/node/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
@@ -1991,10 +2100,6 @@
|
|||||||
|
|
||||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
|
||||||
|
|
||||||
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
|
||||||
|
|
||||||
"serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
"serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
"serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||||
@@ -2021,6 +2126,20 @@
|
|||||||
|
|
||||||
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.16.0", "", { "dependencies": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" } }, "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A=="],
|
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.16.0", "", { "dependencies": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" } }, "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/helpers": ["@babel/helpers@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
"serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ services:
|
|||||||
# === ABSOLUTELY REQUIRED ===
|
# === ABSOLUTELY REQUIRED ===
|
||||||
# This MUST be set and CANNOT be changed via UI
|
# This MUST be set and CANNOT be changed via UI
|
||||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} # Min 32 chars, required for sessions
|
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} # Min 32 chars, required for sessions
|
||||||
|
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321}
|
||||||
|
- BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS:-http://localhost:4321}
|
||||||
|
|
||||||
# === CORE SETTINGS ===
|
# === CORE SETTINGS ===
|
||||||
# These are technically required but have working defaults
|
# These are technically required but have working defaults
|
||||||
@@ -23,7 +25,11 @@ services:
|
|||||||
- DATABASE_URL=file:data/gitea-mirror.db
|
- DATABASE_URL=file:data/gitea-mirror.db
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- PORT=4321
|
- PORT=4321
|
||||||
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321}
|
- PUBLIC_BETTER_AUTH_URL=${PUBLIC_BETTER_AUTH_URL:-http://localhost:4321}
|
||||||
|
# Optional concurrency controls (defaults match in-app defaults)
|
||||||
|
# If you want perfect ordering of issues and PRs, set these at 1
|
||||||
|
- MIRROR_ISSUE_CONCURRENCY=${MIRROR_ISSUE_CONCURRENCY:-3}
|
||||||
|
- MIRROR_PULL_REQUEST_CONCURRENCY=${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
||||||
@@ -52,4 +58,4 @@ services:
|
|||||||
# - Auto-import settings
|
# - Auto-import settings
|
||||||
# - Cleanup preferences
|
# - Cleanup preferences
|
||||||
#
|
#
|
||||||
# That's it! Everything else can be configured via the web interface.
|
# That's it! Everything else can be configured via the web interface.
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ services:
|
|||||||
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
||||||
- ONLY_MIRROR_ORGS=${ONLY_MIRROR_ORGS:-false}
|
- ONLY_MIRROR_ORGS=${ONLY_MIRROR_ORGS:-false}
|
||||||
- SKIP_STARRED_ISSUES=${SKIP_STARRED_ISSUES:-false}
|
- SKIP_STARRED_ISSUES=${SKIP_STARRED_ISSUES:-false}
|
||||||
|
- MIRROR_ISSUE_CONCURRENCY=${MIRROR_ISSUE_CONCURRENCY:-3}
|
||||||
|
- MIRROR_PULL_REQUEST_CONCURRENCY=${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
|
||||||
- GITEA_URL=${GITEA_URL:-}
|
- GITEA_URL=${GITEA_URL:-}
|
||||||
- GITEA_TOKEN=${GITEA_TOKEN:-}
|
- GITEA_TOKEN=${GITEA_TOKEN:-}
|
||||||
- GITEA_USERNAME=${GITEA_USERNAME:-}
|
- GITEA_USERNAME=${GITEA_USERNAME:-}
|
||||||
|
|||||||
@@ -120,161 +120,13 @@ fi
|
|||||||
# Dependencies are already installed during the Docker build process
|
# Dependencies are already installed during the Docker build process
|
||||||
|
|
||||||
# Initialize the database if it doesn't exist
|
# Initialize the database if it doesn't exist
|
||||||
|
# Note: Drizzle migrations will be run automatically when the app starts (see src/lib/db/index.ts)
|
||||||
if [ ! -f "/app/data/gitea-mirror.db" ]; then
|
if [ ! -f "/app/data/gitea-mirror.db" ]; then
|
||||||
echo "Initializing database..."
|
echo "Database not found. It will be created and initialized via Drizzle migrations on first app startup..."
|
||||||
if [ -f "dist/scripts/init-db.js" ]; then
|
# Create empty database file so migrations can run
|
||||||
bun dist/scripts/init-db.js
|
touch /app/data/gitea-mirror.db
|
||||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
|
||||||
bun dist/scripts/manage-db.js init
|
|
||||||
elif [ -f "scripts/manage-db.ts" ]; then
|
|
||||||
bun scripts/manage-db.ts init
|
|
||||||
else
|
|
||||||
echo "Warning: Could not find database initialization scripts in dist/scripts."
|
|
||||||
echo "Creating and initializing database manually..."
|
|
||||||
|
|
||||||
# Create the database file
|
|
||||||
touch /app/data/gitea-mirror.db
|
|
||||||
|
|
||||||
# Initialize the database with required tables
|
|
||||||
sqlite3 /app/data/gitea-mirror.db <<EOF
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
email TEXT NOT NULL,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS configs (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
is_active INTEGER NOT NULL DEFAULT 1,
|
|
||||||
github_config TEXT NOT NULL,
|
|
||||||
gitea_config TEXT NOT NULL,
|
|
||||||
include TEXT NOT NULL DEFAULT '["*"]',
|
|
||||||
exclude TEXT NOT NULL DEFAULT '[]',
|
|
||||||
schedule_config TEXT NOT NULL,
|
|
||||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
||||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS repositories (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
config_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
full_name TEXT NOT NULL,
|
|
||||||
url TEXT NOT NULL,
|
|
||||||
clone_url TEXT NOT NULL,
|
|
||||||
owner TEXT NOT NULL,
|
|
||||||
organization TEXT,
|
|
||||||
mirrored_location TEXT DEFAULT '',
|
|
||||||
destination_org TEXT,
|
|
||||||
is_private INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_fork INTEGER NOT NULL DEFAULT 0,
|
|
||||||
forked_from TEXT,
|
|
||||||
has_issues INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_starred INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
||||||
size INTEGER NOT NULL DEFAULT 0,
|
|
||||||
has_lfs INTEGER NOT NULL DEFAULT 0,
|
|
||||||
has_submodules INTEGER NOT NULL DEFAULT 0,
|
|
||||||
language TEXT,
|
|
||||||
description TEXT,
|
|
||||||
default_branch TEXT NOT NULL,
|
|
||||||
visibility TEXT NOT NULL DEFAULT 'public',
|
|
||||||
status TEXT NOT NULL DEFAULT 'imported',
|
|
||||||
last_mirrored INTEGER,
|
|
||||||
error_message TEXT,
|
|
||||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
||||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Uniqueness of (user_id, full_name) for repositories is enforced via drizzle migrations
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS organizations (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
config_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
avatar_url TEXT NOT NULL,
|
|
||||||
membership_role TEXT NOT NULL DEFAULT 'member',
|
|
||||||
is_included INTEGER NOT NULL DEFAULT 1,
|
|
||||||
status TEXT NOT NULL DEFAULT 'imported',
|
|
||||||
last_mirrored INTEGER,
|
|
||||||
error_message TEXT,
|
|
||||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
||||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS mirror_jobs (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
repository_id TEXT,
|
|
||||||
repository_name TEXT,
|
|
||||||
organization_id TEXT,
|
|
||||||
organization_name TEXT,
|
|
||||||
details TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'imported',
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
-- New fields for job resilience
|
|
||||||
job_type TEXT NOT NULL DEFAULT 'mirror',
|
|
||||||
batch_id TEXT,
|
|
||||||
total_items INTEGER,
|
|
||||||
completed_items INTEGER DEFAULT 0,
|
|
||||||
item_ids TEXT, -- JSON array as text
|
|
||||||
completed_item_ids TEXT DEFAULT '[]', -- JSON array as text
|
|
||||||
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer
|
|
||||||
started_at TIMESTAMP,
|
|
||||||
completed_at TIMESTAMP,
|
|
||||||
last_checkpoint TIMESTAMP,
|
|
||||||
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS events (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
channel TEXT NOT NULL,
|
|
||||||
payload TEXT NOT NULL,
|
|
||||||
read INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_events_read ON events(read);
|
|
||||||
EOF
|
|
||||||
echo "Database initialized with required tables."
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "Database already exists, checking for issues..."
|
echo "Database already exists, Drizzle will check for pending migrations on startup..."
|
||||||
if [ -f "dist/scripts/fix-db-issues.js" ]; then
|
|
||||||
bun dist/scripts/fix-db-issues.js
|
|
||||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
|
||||||
bun dist/scripts/manage-db.js fix
|
|
||||||
elif [ -f "scripts/manage-db.ts" ]; then
|
|
||||||
bun scripts/manage-db.ts fix
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Database exists, checking integrity..."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract version from package.json and set as environment variable
|
# Extract version from package.json and set as environment variable
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
# Better Auth Migration Guide
|
|
||||||
|
|
||||||
This document describes the migration from the legacy authentication system to Better Auth.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Gitea Mirror has been migrated to use Better Auth, a modern authentication library that provides:
|
|
||||||
- Built-in support for email/password authentication
|
|
||||||
- Session management with secure cookies
|
|
||||||
- Database adapter with Drizzle ORM
|
|
||||||
- Ready for OAuth2, OIDC, and SSO integrations
|
|
||||||
- Type-safe authentication throughout the application
|
|
||||||
|
|
||||||
## Key Changes
|
|
||||||
|
|
||||||
### 1. Database Schema
|
|
||||||
|
|
||||||
New tables added:
|
|
||||||
- `sessions` - User session management
|
|
||||||
- `accounts` - Authentication providers (credentials, OAuth, etc.)
|
|
||||||
- `verification_tokens` - Email verification and password reset tokens
|
|
||||||
|
|
||||||
Modified tables:
|
|
||||||
- `users` - Added `emailVerified` field
|
|
||||||
|
|
||||||
### 2. Authentication Flow
|
|
||||||
|
|
||||||
**Login:**
|
|
||||||
- Users now log in with email instead of username
|
|
||||||
- Endpoint: `/api/auth/sign-in/email`
|
|
||||||
- Session cookies are automatically managed
|
|
||||||
|
|
||||||
**Registration:**
|
|
||||||
- Users register with username, email, and password
|
|
||||||
- Username is stored as an additional field
|
|
||||||
- Endpoint: `/api/auth/sign-up/email`
|
|
||||||
|
|
||||||
### 3. API Routes
|
|
||||||
|
|
||||||
All auth routes are now handled by Better Auth's catch-all handler:
|
|
||||||
- `/api/auth/[...all].ts` handles all authentication endpoints
|
|
||||||
|
|
||||||
Legacy routes have been backed up to `/src/pages/api/auth/legacy-backup/`
|
|
||||||
|
|
||||||
### 4. Session Management
|
|
||||||
|
|
||||||
Sessions are now managed by Better Auth:
|
|
||||||
- Middleware automatically populates `context.locals.user` and `context.locals.session`
|
|
||||||
- Use `useAuth()` hook in React components for client-side auth
|
|
||||||
- Sessions expire after 30 days by default
|
|
||||||
|
|
||||||
## Future OIDC/SSO Configuration
|
|
||||||
|
|
||||||
The project is now ready for OIDC and SSO integrations. To enable:
|
|
||||||
|
|
||||||
### 1. Enable SSO Plugin
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/lib/auth.ts
|
|
||||||
import { sso } from "better-auth/plugins/sso";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
// ... existing config
|
|
||||||
plugins: [
|
|
||||||
sso({
|
|
||||||
provisionUser: async (data) => {
|
|
||||||
// Custom user provisioning logic
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Register OIDC Providers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Example: Register an OIDC provider
|
|
||||||
await authClient.sso.register({
|
|
||||||
issuer: "https://idp.example.com",
|
|
||||||
domain: "example.com",
|
|
||||||
clientId: "your-client-id",
|
|
||||||
clientSecret: "your-client-secret",
|
|
||||||
providerId: "example-provider",
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Enable OIDC Provider Mode
|
|
||||||
|
|
||||||
To make Gitea Mirror act as an OIDC provider:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/lib/auth.ts
|
|
||||||
import { oidcProvider } from "better-auth/plugins/oidc";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
// ... existing config
|
|
||||||
plugins: [
|
|
||||||
oidcProvider({
|
|
||||||
loginPage: "/signin",
|
|
||||||
consentPage: "/oauth/consent",
|
|
||||||
metadata: {
|
|
||||||
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Database Migration for SSO
|
|
||||||
|
|
||||||
When enabling SSO/OIDC, run migrations to add required tables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate the schema
|
|
||||||
bun drizzle-kit generate
|
|
||||||
|
|
||||||
# Apply the migration
|
|
||||||
bun drizzle-kit migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
New tables that will be added:
|
|
||||||
- `sso_providers` - SSO provider configurations
|
|
||||||
- `oauth_applications` - OAuth2 client applications
|
|
||||||
- `oauth_access_tokens` - OAuth2 access tokens
|
|
||||||
- `oauth_consents` - User consent records
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Required environment variables:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Better Auth configuration
|
|
||||||
BETTER_AUTH_SECRET=your-secret-key
|
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# Legacy (kept for compatibility)
|
|
||||||
JWT_SECRET=your-secret-key
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Script
|
|
||||||
|
|
||||||
To migrate existing users to Better Auth:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run migrate:better-auth
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
|
||||||
1. Creates credential accounts for existing users
|
|
||||||
2. Moves password hashes to the accounts table
|
|
||||||
3. Preserves user creation dates
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Login Issues
|
|
||||||
- Ensure users log in with email, not username
|
|
||||||
- Check that BETTER_AUTH_SECRET is set
|
|
||||||
- Verify database migrations have been applied
|
|
||||||
|
|
||||||
### Session Issues
|
|
||||||
- Clear browser cookies if experiencing session problems
|
|
||||||
- Check middleware is properly configured
|
|
||||||
- Ensure auth routes are accessible at `/api/auth/*`
|
|
||||||
|
|
||||||
### Development Tips
|
|
||||||
- Use `bun db:studio` to inspect database tables
|
|
||||||
- Check `/api/auth/session` to verify current session
|
|
||||||
- Enable debug logging in Better Auth for troubleshooting
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Better Auth Documentation](https://better-auth.com)
|
|
||||||
- [Better Auth Astro Integration](https://better-auth.com/docs/integrations/astro)
|
|
||||||
- [Better Auth Plugins](https://better-auth.com/docs/plugins)
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
# Build Guide
|
|
||||||
|
|
||||||
This guide covers building the open-source version of Gitea Mirror.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- **Bun** >= 1.2.9 (primary runtime)
|
|
||||||
- **Node.js** >= 20 (for compatibility)
|
|
||||||
- **Git**
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone repository
|
|
||||||
git clone https://github.com/yourusername/gitea-mirror.git
|
|
||||||
cd gitea-mirror
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
bun install
|
|
||||||
|
|
||||||
# Initialize database
|
|
||||||
bun run init-db
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `bun run build` | Production build |
|
|
||||||
| `bun run dev` | Development server |
|
|
||||||
| `bun run preview` | Preview production build |
|
|
||||||
| `bun test` | Run tests |
|
|
||||||
| `bun run cleanup-db` | Remove database files |
|
|
||||||
|
|
||||||
## Build Output
|
|
||||||
|
|
||||||
The build creates:
|
|
||||||
- `dist/` - Production-ready server files
|
|
||||||
- `.astro/` - Build cache (git-ignored)
|
|
||||||
- `data/` - SQLite database location
|
|
||||||
|
|
||||||
## Development Build
|
|
||||||
|
|
||||||
For active development with hot reload:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Access the application at http://localhost:4321
|
|
||||||
|
|
||||||
## Production Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
# Test the build
|
|
||||||
bun run preview
|
|
||||||
|
|
||||||
# Run in production
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Build
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Build Docker image
|
|
||||||
docker build -t gitea-mirror:latest .
|
|
||||||
|
|
||||||
# Run container
|
|
||||||
docker run -p 3000:3000 gitea-mirror:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Create a `.env` file:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Database
|
|
||||||
DATABASE_PATH=./data/gitea-mirror.db
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
JWT_SECRET=your-secret-here
|
|
||||||
|
|
||||||
# GitHub Configuration
|
|
||||||
GITHUB_TOKEN=ghp_...
|
|
||||||
GITHUB_WEBHOOK_SECRET=...
|
|
||||||
GITHUB_EXCLUDED_ORGS=org1,org2,org3 # Optional: Comma-separated list of organizations to exclude from sync
|
|
||||||
|
|
||||||
# Gitea Configuration
|
|
||||||
GITEA_URL=https://your-gitea.com
|
|
||||||
GITEA_TOKEN=...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Build Issues
|
|
||||||
|
|
||||||
### Missing Dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Solution
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Not Initialized
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Solution
|
|
||||||
bun run init-db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port Already in Use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Change port
|
|
||||||
PORT=3001 bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Cache Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clear cache
|
|
||||||
rm -rf .astro/ dist/
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Optimization
|
|
||||||
|
|
||||||
### Development Speed
|
|
||||||
|
|
||||||
- Use `bun run dev` for hot reload
|
|
||||||
- Skip type checking during rapid development
|
|
||||||
- Keep `.astro/` cache between builds
|
|
||||||
|
|
||||||
### Production Optimization
|
|
||||||
|
|
||||||
- Minification enabled automatically
|
|
||||||
- Tree shaking removes unused code
|
|
||||||
- Image optimization with Sharp
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
After building, verify:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check build output
|
|
||||||
ls -la dist/
|
|
||||||
|
|
||||||
# Test server starts
|
|
||||||
bun run start
|
|
||||||
|
|
||||||
# Check health endpoint
|
|
||||||
curl http://localhost:3000/api/health
|
|
||||||
```
|
|
||||||
|
|
||||||
## CI/CD Build
|
|
||||||
|
|
||||||
Example GitHub Actions workflow:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Build and Test
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
- run: bun install
|
|
||||||
- run: bun run build
|
|
||||||
- run: bun test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Build Fails
|
|
||||||
|
|
||||||
1. Check Bun version: `bun --version`
|
|
||||||
2. Clear dependencies: `rm -rf node_modules && bun install`
|
|
||||||
3. Check for syntax errors: `bunx tsc --noEmit`
|
|
||||||
|
|
||||||
### Runtime Errors
|
|
||||||
|
|
||||||
1. Check environment variables
|
|
||||||
2. Verify database exists
|
|
||||||
3. Check file permissions
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
Expected build times:
|
|
||||||
- Clean build: ~5-10 seconds
|
|
||||||
- Incremental build: ~2-5 seconds
|
|
||||||
- Development startup: ~1-2 seconds
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- Configure with [Configuration Guide](./CONFIGURATION.md)
|
|
||||||
- Deploy with [Deployment Guide](./DEPLOYMENT.md)
|
|
||||||
- Set up authentication with [SSO Guide](./SSO-OIDC-SETUP.md)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../certs/README.md
|
|
||||||
@@ -16,27 +16,22 @@ This guide covers the development workflow for the open-source Gitea Mirror.
|
|||||||
|
|
||||||
1. **Clone the repository**:
|
1. **Clone the repository**:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/yourusername/gitea-mirror.git
|
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||||
cd gitea-mirror
|
cd gitea-mirror
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies**:
|
2. **Install dependencies and seed the SQLite database**:
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun run setup
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Initialize database**:
|
3. **Configure environment (optional)**:
|
||||||
```bash
|
|
||||||
bun run init-db
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Configure environment**:
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with your settings
|
# Edit .env with your settings
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Start development server**:
|
4. **Start the development server**:
|
||||||
```bash
|
```bash
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
@@ -45,29 +40,33 @@ bun run dev
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `bun run dev` | Start development server with hot reload |
|
| `bun run dev` | Start the Bun + Astro dev server with hot reload |
|
||||||
| `bun run build` | Build for production |
|
| `bun run build` | Build the production bundle |
|
||||||
| `bun run preview` | Preview production build |
|
| `bun run preview` | Preview the production build locally |
|
||||||
| `bun test` | Run all tests |
|
| `bun test` | Run the Bun test suite |
|
||||||
| `bun test:watch` | Run tests in watch mode |
|
| `bun test:watch` | Run tests in watch mode |
|
||||||
| `bun run db:studio` | Open database GUI |
|
| `bun run db:studio` | Launch Drizzle Kit Studio |
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
gitea-mirror/
|
gitea-mirror/
|
||||||
├── src/
|
├── src/ # Application UI, API routes, and services
|
||||||
│ ├── components/ # React components
|
│ ├── components/ # React components rendered inside Astro pages
|
||||||
│ ├── pages/ # Astro pages & API routes
|
│ ├── pages/ # Astro pages and API routes (e.g., /api/*)
|
||||||
│ ├── lib/ # Core logic
|
│ ├── lib/ # Core logic: GitHub/Gitea clients, scheduler, recovery, db helpers
|
||||||
│ │ ├── db/ # Database queries
|
│ │ ├── db/ # Drizzle adapter + schema
|
||||||
│ │ ├── utils/ # Helper functions
|
│ │ ├── modules/ # Module wiring (jobs, integrations)
|
||||||
│ │ └── modules/ # Module system
|
│ │ └── utils/ # Shared utilities
|
||||||
│ ├── hooks/ # React hooks
|
│ ├── hooks/ # React hooks
|
||||||
│ └── types/ # TypeScript types
|
│ ├── content/ # In-app documentation and templated content
|
||||||
├── public/ # Static assets
|
│ ├── layouts/ # Shared layout components
|
||||||
├── scripts/ # Utility scripts
|
│ ├── styles/ # Tailwind CSS entrypoints
|
||||||
└── tests/ # Test files
|
│ └── types/ # TypeScript types
|
||||||
|
├── scripts/ # Bun scripts for DB management and maintenance
|
||||||
|
├── www/ # Marketing site (Astro + MDX use cases)
|
||||||
|
├── public/ # Static assets served by Vite/Astro
|
||||||
|
└── tests/ # Dedicated integration/unit test helpers
|
||||||
```
|
```
|
||||||
|
|
||||||
## Feature Development
|
## Feature Development
|
||||||
@@ -80,10 +79,10 @@ git checkout -b feature/my-feature
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Plan your changes**:
|
2. **Plan your changes**:
|
||||||
- UI components in `/src/components/`
|
- UI components live in `src/components/`
|
||||||
- API endpoints in `/src/pages/api/`
|
- API endpoints live in `src/pages/api/`
|
||||||
- Database queries in `/src/lib/db/queries/`
|
- Database logic is under `src/lib/db/` (schema + adapter)
|
||||||
- Types in `/src/types/`
|
- Shared types are in `src/types/`
|
||||||
|
|
||||||
3. **Implement the feature**:
|
3. **Implement the feature**:
|
||||||
|
|
||||||
@@ -120,7 +119,7 @@ describe('My Feature', () => {
|
|||||||
|
|
||||||
5. **Update documentation**:
|
5. **Update documentation**:
|
||||||
- Add JSDoc comments
|
- Add JSDoc comments
|
||||||
- Update README if needed
|
- Update README/docs if needed
|
||||||
- Document API changes
|
- Document API changes
|
||||||
|
|
||||||
## Database Development
|
## Database Development
|
||||||
@@ -352,4 +351,4 @@ git push origin v2.23.0
|
|||||||
|
|
||||||
- Check existing [issues](https://github.com/yourusername/gitea-mirror/issues)
|
- Check existing [issues](https://github.com/yourusername/gitea-mirror/issues)
|
||||||
- Join [discussions](https://github.com/yourusername/gitea-mirror/discussions)
|
- Join [discussions](https://github.com/yourusername/gitea-mirror/discussions)
|
||||||
- Read the [FAQ](./FAQ.md)
|
- Read the [FAQ](./FAQ.md)
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ Control what content gets mirrored from GitHub to Gitea.
|
|||||||
| `MIRROR_PULL_REQUESTS` | Mirror pull requests (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
| `MIRROR_PULL_REQUESTS` | Mirror pull requests (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||||
| `MIRROR_LABELS` | Mirror labels (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
| `MIRROR_LABELS` | Mirror labels (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||||
| `MIRROR_MILESTONES` | Mirror milestones (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
| `MIRROR_MILESTONES` | Mirror milestones (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_ISSUE_CONCURRENCY` | Number of issues processed in parallel. Set above `1` to speed up mirroring at the risk of out-of-order creation. | `3` | Integer ≥ 1 |
|
||||||
|
| `MIRROR_PULL_REQUEST_CONCURRENCY` | Number of pull requests processed in parallel. Values above `1` may cause ordering differences. | `5` | Integer ≥ 1 |
|
||||||
|
|
||||||
|
> **Ordering vs Throughput:** Metadata now mirrors sequentially by default to preserve chronology. Increase the concurrency variables only if you can tolerate minor out-of-order entries.
|
||||||
|
|
||||||
## Automation Configuration
|
## Automation Configuration
|
||||||
|
|
||||||
@@ -229,7 +233,7 @@ Configure automatic cleanup of old events and data.
|
|||||||
| `CLEANUP_DELETE_FROM_GITEA` | Delete repositories from Gitea | `false` | `true`, `false` |
|
| `CLEANUP_DELETE_FROM_GITEA` | Delete repositories from Gitea | `false` | `true`, `false` |
|
||||||
| `CLEANUP_DELETE_IF_NOT_IN_GITHUB` | Delete repos not found in GitHub (automatically enables cleanup) | `true` | `true`, `false` |
|
| `CLEANUP_DELETE_IF_NOT_IN_GITHUB` | Delete repos not found in GitHub (automatically enables cleanup) | `true` | `true`, `false` |
|
||||||
| `CLEANUP_ORPHANED_REPO_ACTION` | Action for orphaned repositories. **Note**: `archive` is recommended to preserve backups | `archive` | `skip`, `archive`, `delete` |
|
| `CLEANUP_ORPHANED_REPO_ACTION` | Action for orphaned repositories. **Note**: `archive` is recommended to preserve backups | `archive` | `skip`, `archive`, `delete` |
|
||||||
| `CLEANUP_DRY_RUN` | Test mode without actual deletion | `true` | `true`, `false` |
|
| `CLEANUP_DRY_RUN` | Test mode without actual deletion | `false` | `true`, `false` |
|
||||||
| `CLEANUP_PROTECTED_REPOS` | Comma-separated list of protected repository names | - | Comma-separated strings |
|
| `CLEANUP_PROTECTED_REPOS` | Comma-separated list of protected repository names | - | Comma-separated strings |
|
||||||
|
|
||||||
**🛡️ Safety Features (Backup Protection)**:
|
**🛡️ Safety Features (Backup Protection)**:
|
||||||
@@ -242,10 +246,11 @@ Configure automatic cleanup of old events and data.
|
|||||||
- **Regular repositories**: Uses Gitea's native archive feature (PATCH `/repos/{owner}/{repo}` with `archived: true`)
|
- **Regular repositories**: Uses Gitea's native archive feature (PATCH `/repos/{owner}/{repo}` with `archived: true`)
|
||||||
- Makes repository read-only while preserving all data
|
- Makes repository read-only while preserving all data
|
||||||
- **Mirror repositories**: Uses rename strategy (Gitea API returns 422 for archiving mirrors)
|
- **Mirror repositories**: Uses rename strategy (Gitea API returns 422 for archiving mirrors)
|
||||||
- Renamed with `[ARCHIVED]` prefix for clear identification
|
- Renamed with `archived-` prefix for clear identification
|
||||||
- Description updated with preservation notice and timestamp
|
- Description updated with preservation notice and timestamp
|
||||||
- Mirror interval set to 8760h (1 year) to minimize sync attempts
|
- Mirror interval set to 8760h (1 year) to minimize sync attempts
|
||||||
- Repository remains fully accessible and cloneable
|
- Repository remains fully accessible and cloneable
|
||||||
|
- **Manual Sync Option**: Archived mirrors are still available on the Repositories page with automatic syncs disabled—use the `Manual Sync` action to refresh them on demand.
|
||||||
|
|
||||||
### Execution Settings
|
### Execution Settings
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
# Extending Gitea Mirror
|
|
||||||
|
|
||||||
Gitea Mirror is designed with extensibility in mind through a module system.
|
|
||||||
|
|
||||||
## Module System
|
|
||||||
|
|
||||||
The application provides a module interface that allows extending functionality:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface Module {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
init(app: AppContext): Promise<void>;
|
|
||||||
cleanup?(): Promise<void>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Creating Custom Modules
|
|
||||||
|
|
||||||
You can create custom modules to add features:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// my-module.ts
|
|
||||||
export class MyModule implements Module {
|
|
||||||
name = 'my-module';
|
|
||||||
version = '1.0.0';
|
|
||||||
|
|
||||||
async init(app: AppContext) {
|
|
||||||
// Add your functionality
|
|
||||||
app.addRoute('/api/my-endpoint', this.handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handler(context) {
|
|
||||||
return new Response('Hello from my module!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module Context
|
|
||||||
|
|
||||||
Modules receive an `AppContext` with:
|
|
||||||
- Database access
|
|
||||||
- Event system
|
|
||||||
- Route registration
|
|
||||||
- Configuration
|
|
||||||
|
|
||||||
## Private Extensions
|
|
||||||
|
|
||||||
If you're developing private extensions:
|
|
||||||
|
|
||||||
1. Create a separate package/repository
|
|
||||||
2. Implement the module interface
|
|
||||||
3. Use Bun's linking feature for development:
|
|
||||||
```bash
|
|
||||||
# In your extension
|
|
||||||
bun link
|
|
||||||
|
|
||||||
# In gitea-mirror
|
|
||||||
bun link your-extension
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
- Keep modules focused on a single feature
|
|
||||||
- Use TypeScript for type safety
|
|
||||||
- Handle errors gracefully
|
|
||||||
- Clean up resources in `cleanup()`
|
|
||||||
- Document your module's API
|
|
||||||
|
|
||||||
## Community Modules
|
|
||||||
|
|
||||||
Share your modules with the community:
|
|
||||||
- Create a GitHub repository
|
|
||||||
- Tag it with `gitea-mirror-module`
|
|
||||||
- Submit a PR to list it in our docs
|
|
||||||
|
|
||||||
For more details on the module system, see the source code in `/src/lib/modules/`.
|
|
||||||
125
docs/README.md
@@ -1,118 +1,39 @@
|
|||||||
# Gitea Mirror Documentation
|
# Gitea Mirror Documentation
|
||||||
|
|
||||||
Welcome to the Gitea Mirror documentation. This guide covers everything you need to know about developing, building, and deploying the open-source version of Gitea Mirror.
|
This folder contains engineering and operations references for the open-source Gitea Mirror project. Each guide focuses on the parts of the system that still require bespoke explanation beyond the in-app help and the main `README.md`.
|
||||||
|
|
||||||
## Documentation Overview
|
## Available Guides
|
||||||
|
|
||||||
### Getting Started
|
### Core workflow
|
||||||
|
- **[DEVELOPMENT_WORKFLOW.md](./DEVELOPMENT_WORKFLOW.md)** – Set up a local environment, run scripts, and understand the repo layout (app + marketing site).
|
||||||
|
- **[ENVIRONMENT_VARIABLES.md](./ENVIRONMENT_VARIABLES.md)** – Complete reference for every configuration flag supported by the app and Docker images.
|
||||||
|
|
||||||
- **[Development Workflow](./DEVELOPMENT_WORKFLOW.md)** - Set up your development environment and start contributing
|
### Reliability & recovery
|
||||||
- **[Build Guide](./BUILD_GUIDE.md)** - Build Gitea Mirror from source
|
- **[GRACEFUL_SHUTDOWN.md](./GRACEFUL_SHUTDOWN.md)** – How signal handling, shutdown coordination, and job persistence work in v3.
|
||||||
- **[Configuration Guide](./CONFIGURATION.md)** - Configure all available options
|
- **[RECOVERY_IMPROVEMENTS.md](./RECOVERY_IMPROVEMENTS.md)** – Deep dive into the startup recovery workflow and supporting scripts.
|
||||||
|
|
||||||
### Deployment
|
### Authentication
|
||||||
|
- **[SSO-OIDC-SETUP.md](./SSO-OIDC-SETUP.md)** – Configure OIDC/SSO providers through the admin UI.
|
||||||
|
- **[SSO_TESTING.md](./SSO_TESTING.md)** – Recipes for local and staging SSO testing (Google, Keycloak, mock providers).
|
||||||
|
|
||||||
- **[Deployment Guide](./DEPLOYMENT.md)** - Deploy to production environments
|
If you are looking for customer-facing playbooks, see the MDX use cases under `www/src/pages/use-cases/`.
|
||||||
- **[Docker Guide](./DOCKER.md)** - Container-based deployment
|
|
||||||
- **[Reverse Proxy Setup](./REVERSE_PROXY.md)** - Configure with nginx/Caddy
|
|
||||||
|
|
||||||
### Features
|
## Quick start for local development
|
||||||
|
|
||||||
- **[SSO/OIDC Setup](./SSO-OIDC-SETUP.md)** - Configure authentication providers
|
|
||||||
- **[Sponsor Integration](./SPONSOR_INTEGRATION.md)** - GitHub Sponsors integration
|
|
||||||
- **[Webhook Configuration](./WEBHOOKS.md)** - Set up GitHub webhooks
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
- **[Architecture Overview](./ARCHITECTURE.md)** - System design and components
|
|
||||||
- **[API Documentation](./API.md)** - REST API endpoints
|
|
||||||
- **[Database Schema](./DATABASE.md)** - SQLite structure
|
|
||||||
|
|
||||||
### Maintenance
|
|
||||||
|
|
||||||
- **[Migration Guide](../MIGRATION_GUIDE.md)** - Upgrade from previous versions
|
|
||||||
- **[Better Auth Migration](./BETTER_AUTH_MIGRATION.md)** - Migrate authentication system
|
|
||||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions
|
|
||||||
- **[Backup & Restore](./BACKUP.md)** - Data management
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
1. **Clone and install**:
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/yourusername/gitea-mirror.git
|
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||||
cd gitea-mirror
|
cd gitea-mirror
|
||||||
bun install
|
bun run setup # installs deps and seeds the SQLite DB
|
||||||
|
bun run dev # starts the Astro/Bun app on http://localhost:4321
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Configure**:
|
The first user you create locally becomes the administrator. All other configuration—GitHub owners, Gitea targets, scheduling, cleanup—is done through the **Configuration** screen in the UI.
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your GitHub and Gitea tokens
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Initialize and run**:
|
## Contributing & support
|
||||||
```bash
|
|
||||||
bun run init-db
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Access**: Open http://localhost:4321
|
- 🎯 Contribution guide: [../CONTRIBUTING.md](../CONTRIBUTING.md)
|
||||||
|
- 📘 Code of conduct: [../CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)
|
||||||
|
- 🐞 Issues & feature requests: <https://github.com/RayLabsHQ/gitea-mirror/issues>
|
||||||
|
- 💬 Discussions: <https://github.com/RayLabsHQ/gitea-mirror/discussions>
|
||||||
|
|
||||||
## Key Features
|
Security disclosures should follow the process in [../SECURITY.md](../SECURITY.md).
|
||||||
|
|
||||||
- 🔄 **Automatic Syncing** - Keep repositories synchronized
|
|
||||||
- 🗂️ **Organization Support** - Mirror entire organizations
|
|
||||||
- ⭐ **Starred Repos** - Mirror your starred repositories
|
|
||||||
- 🔐 **Self-Hosted** - Full control over your data
|
|
||||||
- 🚀 **Fast** - Built with Bun for optimal performance
|
|
||||||
- 🔒 **Secure** - JWT authentication, encrypted tokens
|
|
||||||
|
|
||||||
## Technology Stack
|
|
||||||
|
|
||||||
- **Runtime**: Bun
|
|
||||||
- **Framework**: Astro with React
|
|
||||||
- **Database**: SQLite with Drizzle ORM
|
|
||||||
- **Styling**: Tailwind CSS v4
|
|
||||||
- **Authentication**: Better Auth
|
|
||||||
|
|
||||||
## System Requirements
|
|
||||||
|
|
||||||
- Bun >= 1.2.9
|
|
||||||
- Node.js >= 20 (optional, for compatibility)
|
|
||||||
- SQLite 3
|
|
||||||
- 512MB RAM minimum
|
|
||||||
- 1GB disk space
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
We welcome contributions! Please see our [Contributing Guide](../CONTRIBUTING.md) for details.
|
|
||||||
|
|
||||||
### Development Setup
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Make your changes
|
|
||||||
4. Add tests
|
|
||||||
5. Submit a pull request
|
|
||||||
|
|
||||||
### Code of Conduct
|
|
||||||
|
|
||||||
Please read our [Code of Conduct](../CODE_OF_CONDUCT.md) before contributing.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- **Issues**: [GitHub Issues](https://github.com/yourusername/gitea-mirror/issues)
|
|
||||||
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/gitea-mirror/discussions)
|
|
||||||
- **Wiki**: [GitHub Wiki](https://github.com/yourusername/gitea-mirror/wiki)
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
For security issues, please see [SECURITY.md](../SECURITY.md).
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Gitea Mirror is open source software licensed under the [MIT License](../LICENSE).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For detailed information on any topic, please refer to the specific documentation guides listed above.
|
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
# Graceful Shutdown Process
|
|
||||||
|
|
||||||
This document details how the gitea-mirror application handles graceful shutdown during active mirroring operations, with specific focus on job interruption and recovery.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The graceful shutdown system is designed for **fast, clean termination** without waiting for long-running jobs to complete. It prioritizes **quick shutdown times** (under 30 seconds) while **preserving all progress** for seamless recovery.
|
|
||||||
|
|
||||||
## Key Principle
|
|
||||||
|
|
||||||
**The application does NOT wait for jobs to finish before shutting down.** Instead, it saves the current state and resumes after restart.
|
|
||||||
|
|
||||||
## Shutdown Scenario Example
|
|
||||||
|
|
||||||
### Initial State
|
|
||||||
- **Job**: Mirror 500 repositories
|
|
||||||
- **Progress**: 200 repositories completed
|
|
||||||
- **Remaining**: 300 repositories pending
|
|
||||||
- **Action**: User initiates shutdown (SIGTERM, Ctrl+C, Docker stop)
|
|
||||||
|
|
||||||
### Shutdown Process (Under 30 seconds)
|
|
||||||
|
|
||||||
#### Step 1: Signal Detection (Immediate)
|
|
||||||
```
|
|
||||||
📡 Received SIGTERM signal
|
|
||||||
🛑 Graceful shutdown initiated by signal: SIGTERM
|
|
||||||
📊 Shutdown status: 1 active jobs, 2 callbacks
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Job State Saving (1-10 seconds)
|
|
||||||
```
|
|
||||||
📝 Step 1: Saving active job states...
|
|
||||||
Saving state for job abc-123...
|
|
||||||
✅ Saved state for job abc-123
|
|
||||||
```
|
|
||||||
|
|
||||||
**What gets saved:**
|
|
||||||
- `inProgress: false` - Mark job as not currently running
|
|
||||||
- `completedItems: 200` - Number of repos successfully mirrored
|
|
||||||
- `totalItems: 500` - Total repos in the job
|
|
||||||
- `completedItemIds: [repo1, repo2, ..., repo200]` - List of completed repos
|
|
||||||
- `itemIds: [repo1, repo2, ..., repo500]` - Full list of repos
|
|
||||||
- `lastCheckpoint: 2025-05-24T17:30:00Z` - Exact shutdown time
|
|
||||||
- `message: "Job interrupted by application shutdown - will resume on restart"`
|
|
||||||
- `status: "imported"` - Keeps status as resumable (not "failed")
|
|
||||||
|
|
||||||
#### Step 3: Service Cleanup (1-5 seconds)
|
|
||||||
```
|
|
||||||
🔧 Step 2: Executing shutdown callbacks...
|
|
||||||
🛑 Shutting down cleanup service...
|
|
||||||
✅ Cleanup service stopped
|
|
||||||
✅ Shutdown callback 1 completed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 4: Clean Exit (Immediate)
|
|
||||||
```
|
|
||||||
💾 Step 3: Closing database connections...
|
|
||||||
✅ Graceful shutdown completed successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
**Total shutdown time: ~15 seconds** (well under the 30-second limit)
|
|
||||||
|
|
||||||
## What Happens to the Remaining 300 Repos?
|
|
||||||
|
|
||||||
### During Shutdown
|
|
||||||
- **NOT processed** - The remaining 300 repos are not mirrored
|
|
||||||
- **NOT lost** - Their IDs are preserved in the job state
|
|
||||||
- **NOT marked as failed** - Job status remains "imported" for recovery
|
|
||||||
|
|
||||||
### After Restart
|
|
||||||
The recovery system automatically:
|
|
||||||
|
|
||||||
1. **Detects interrupted job** during startup
|
|
||||||
2. **Calculates remaining work**: 500 - 200 = 300 repos
|
|
||||||
3. **Extracts remaining repo IDs**: repos 201-500 from the original list
|
|
||||||
4. **Resumes processing** from exactly where it left off
|
|
||||||
5. **Continues until completion** of all 500 repos
|
|
||||||
|
|
||||||
## Timeout Configuration
|
|
||||||
|
|
||||||
### Shutdown Timeouts
|
|
||||||
```typescript
|
|
||||||
const SHUTDOWN_TIMEOUT = 30000; // 30 seconds max shutdown time
|
|
||||||
const JOB_SAVE_TIMEOUT = 10000; // 10 seconds to save job state
|
|
||||||
```
|
|
||||||
|
|
||||||
### Timeout Behavior
|
|
||||||
- **Normal case**: Shutdown completes in 10-20 seconds
|
|
||||||
- **Slow database**: Up to 30 seconds allowed
|
|
||||||
- **Timeout exceeded**: Force exit with code 1
|
|
||||||
- **Container kill**: Orchestrator should allow 45+ seconds grace period
|
|
||||||
|
|
||||||
## Job State Persistence
|
|
||||||
|
|
||||||
### Database Schema
|
|
||||||
The `mirror_jobs` table stores complete job state:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Job identification
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
job_type TEXT NOT NULL DEFAULT 'mirror',
|
|
||||||
|
|
||||||
-- Progress tracking
|
|
||||||
total_items INTEGER,
|
|
||||||
completed_items INTEGER DEFAULT 0,
|
|
||||||
item_ids TEXT, -- JSON array of all repo IDs
|
|
||||||
completed_item_ids TEXT DEFAULT '[]', -- JSON array of completed repo IDs
|
|
||||||
|
|
||||||
-- State management
|
|
||||||
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean: currently running
|
|
||||||
started_at TIMESTAMP,
|
|
||||||
completed_at TIMESTAMP,
|
|
||||||
last_checkpoint TIMESTAMP, -- Last progress save
|
|
||||||
|
|
||||||
-- Status and messaging
|
|
||||||
status TEXT NOT NULL DEFAULT 'imported',
|
|
||||||
message TEXT NOT NULL
|
|
||||||
```
|
|
||||||
|
|
||||||
### Recovery Query
|
|
||||||
The recovery system finds interrupted jobs:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT * FROM mirror_jobs
|
|
||||||
WHERE in_progress = 0
|
|
||||||
AND status = 'imported'
|
|
||||||
AND completed_at IS NULL
|
|
||||||
AND total_items > completed_items;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Shutdown-Aware Processing
|
|
||||||
|
|
||||||
### Concurrency Check
|
|
||||||
During job execution, each repo processing checks for shutdown:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before processing each repository
|
|
||||||
if (isShuttingDown()) {
|
|
||||||
throw new Error('Processing interrupted by application shutdown');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checkpoint Intervals
|
|
||||||
Jobs save progress periodically (every 10 repos by default):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
checkpointInterval: 10, // Save progress every 10 repositories
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures minimal work loss even if shutdown occurs between checkpoints.
|
|
||||||
|
|
||||||
## Container Integration
|
|
||||||
|
|
||||||
### Docker Entrypoint
|
|
||||||
The Docker entrypoint properly forwards signals:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set up signal handlers
|
|
||||||
trap 'shutdown_handler' TERM INT HUP
|
|
||||||
|
|
||||||
# Start application in background
|
|
||||||
bun ./dist/server/entry.mjs &
|
|
||||||
APP_PID=$!
|
|
||||||
|
|
||||||
# Wait for application to finish
|
|
||||||
wait "$APP_PID"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kubernetes Configuration
|
|
||||||
Recommended pod configuration:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
spec:
|
|
||||||
terminationGracePeriodSeconds: 45 # Allow time for graceful shutdown
|
|
||||||
containers:
|
|
||||||
- name: gitea-mirror
|
|
||||||
# ... other configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring and Logging
|
|
||||||
|
|
||||||
### Shutdown Logs
|
|
||||||
```
|
|
||||||
🛑 Graceful shutdown initiated by signal: SIGTERM
|
|
||||||
📊 Shutdown status: 1 active jobs, 2 callbacks
|
|
||||||
📝 Step 1: Saving active job states...
|
|
||||||
Saving state for 1 active jobs...
|
|
||||||
✅ Completed saving all active jobs
|
|
||||||
🔧 Step 2: Executing shutdown callbacks...
|
|
||||||
✅ Completed all shutdown callbacks
|
|
||||||
💾 Step 3: Closing database connections...
|
|
||||||
✅ Graceful shutdown completed successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
### Recovery Logs
|
|
||||||
```
|
|
||||||
⚠️ Jobs found that need recovery. Starting recovery process...
|
|
||||||
Resuming job abc-123 with 300 remaining items...
|
|
||||||
✅ Recovery completed successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### For Operations
|
|
||||||
1. **Monitor shutdown times** - Should complete under 30 seconds
|
|
||||||
2. **Check recovery logs** - Verify jobs resume correctly after restart
|
|
||||||
3. **Set appropriate grace periods** - Allow 45+ seconds in orchestrators
|
|
||||||
4. **Plan maintenance windows** - Jobs will resume but may take time to complete
|
|
||||||
|
|
||||||
### For Development
|
|
||||||
1. **Test shutdown scenarios** - Use `bun run test-shutdown`
|
|
||||||
2. **Monitor job progress** - Check checkpoint frequency and timing
|
|
||||||
3. **Verify recovery** - Ensure interrupted jobs resume correctly
|
|
||||||
4. **Handle edge cases** - Test shutdown during different job phases
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Shutdown Takes Too Long
|
|
||||||
- **Check**: Database performance during job state saving
|
|
||||||
- **Solution**: Increase `SHUTDOWN_TIMEOUT` environment variable
|
|
||||||
- **Monitor**: Job complexity and checkpoint frequency
|
|
||||||
|
|
||||||
### Jobs Don't Resume
|
|
||||||
- **Check**: Recovery logs for errors during startup
|
|
||||||
- **Verify**: Database contains interrupted jobs with correct status
|
|
||||||
- **Test**: Run `bun run startup-recovery` manually
|
|
||||||
|
|
||||||
### Container Force-Killed
|
|
||||||
- **Check**: Container orchestrator termination grace period
|
|
||||||
- **Increase**: Grace period to 45+ seconds
|
|
||||||
- **Monitor**: Application shutdown completion time
|
|
||||||
|
|
||||||
This design ensures **production-ready graceful shutdown** with **zero data loss** and **fast recovery times** suitable for modern containerized deployments.
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# GitHub Sponsors Integration
|
|
||||||
|
|
||||||
This guide shows how GitHub Sponsors is integrated into the open-source version of Gitea Mirror.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
### GitHubSponsors Card
|
|
||||||
|
|
||||||
A card component that displays in the sidebar or dashboard:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { GitHubSponsors } from '@/components/sponsors/GitHubSponsors';
|
|
||||||
|
|
||||||
// In your layout or dashboard
|
|
||||||
<GitHubSponsors />
|
|
||||||
```
|
|
||||||
|
|
||||||
### SponsorButton
|
|
||||||
|
|
||||||
A smaller button for headers or navigation:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { SponsorButton } from '@/components/sponsors/GitHubSponsors';
|
|
||||||
|
|
||||||
// In your header
|
|
||||||
<SponsorButton />
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
### 1. Dashboard Sidebar
|
|
||||||
|
|
||||||
Add the sponsor card to the dashboard sidebar for visibility:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// src/components/layout/DashboardLayout.tsx
|
|
||||||
<aside>
|
|
||||||
{/* Other sidebar content */}
|
|
||||||
<GitHubSponsors />
|
|
||||||
</aside>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Header Navigation
|
|
||||||
|
|
||||||
Add the sponsor button to the main navigation:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// src/components/layout/Header.tsx
|
|
||||||
<nav>
|
|
||||||
{/* Other nav items */}
|
|
||||||
<SponsorButton />
|
|
||||||
</nav>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Settings Page
|
|
||||||
|
|
||||||
Add a support section in settings:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// src/components/settings/SupportSection.tsx
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Support Development</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<GitHubSponsors />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
|
|
||||||
- **Only appears in self-hosted mode**: The components automatically hide in hosted mode
|
|
||||||
- **Non-intrusive**: Designed to be helpful without being annoying
|
|
||||||
- **Multiple options**: GitHub Sponsors, Buy Me a Coffee, and starring the repo
|
|
||||||
|
|
||||||
## Customization
|
|
||||||
|
|
||||||
You can customize the sponsor components by:
|
|
||||||
|
|
||||||
1. Updating the GitHub Sponsors URL
|
|
||||||
2. Adding/removing donation platforms
|
|
||||||
3. Changing the styling to match your theme
|
|
||||||
4. Adjusting the placement based on user feedback
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Don't be pushy**: Show sponsor options tastefully
|
|
||||||
2. **Provide value first**: Ensure the tool is useful before asking for support
|
|
||||||
3. **Be transparent**: Explain how sponsorships help the project
|
|
||||||
4. **Thank sponsors**: Acknowledge supporters in README or releases
|
|
||||||
@@ -81,6 +81,26 @@ Replace `{provider-id}` with your chosen Provider ID.
|
|||||||
- Client Secret: [Your Okta Client Secret]
|
- Client Secret: [Your Okta Client Secret]
|
||||||
- Click "Discover" to auto-fill endpoints
|
- Click "Discover" to auto-fill endpoints
|
||||||
|
|
||||||
|
### Example: Authentik SSO Setup
|
||||||
|
|
||||||
|
Working Authentik deployments (see [#134](https://github.com/RayLabsHQ/gitea-mirror/issues/134)) follow these steps:
|
||||||
|
|
||||||
|
1. In Authentik, create a new **Application** and OIDC **Provider** (implicit flow works well for testing).
|
||||||
|
2. Start creating an SSO provider inside Gitea Mirror so you can copy the redirect URL shown (`https://your-domain.com/api/auth/sso/callback/authentik` if you pick `authentik` as your Provider ID).
|
||||||
|
3. Paste that redirect URL into the Authentik Provider configuration and finish creating the provider.
|
||||||
|
4. Copy the Authentik issuer URL, client ID, and client secret.
|
||||||
|
5. Back in Gitea Mirror:
|
||||||
|
- Issuer URL: the exact value from Authentik (keep any trailing slash Authentik shows).
|
||||||
|
- Provider ID: match the one you used in step 2.
|
||||||
|
- Click **Discover** so Gitea Mirror stores the authorization, token, and JWKS endpoints (Authentik publishes them via discovery).
|
||||||
|
- Domain: enter the email domain you expect to match (e.g. `example.com`).
|
||||||
|
6. Save the provider and test the login flow.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Make sure `BETTER_AUTH_URL` and (if you serve the UI from multiple origins) `BETTER_AUTH_TRUSTED_ORIGINS` point at the public URL users reach. A mismatch can surface as 500 errors after redirect.
|
||||||
|
- Authentik must report the user’s email as verified (default behavior) so Gitea Mirror can auto-link accounts.
|
||||||
|
- If you created an Authentik provider before v3.8.10 you should delete it and re-add it after upgrading; older versions saved incomplete endpoint data which leads to the `url.startsWith` error explained in the Troubleshooting section.
|
||||||
|
|
||||||
## Setting up OIDC Provider
|
## Setting up OIDC Provider
|
||||||
|
|
||||||
The OIDC Provider feature allows other applications to use Gitea Mirror as their authentication provider.
|
The OIDC Provider feature allows other applications to use Gitea Mirror as their authentication provider.
|
||||||
@@ -165,6 +185,7 @@ When an application requests authentication:
|
|||||||
1. **"Invalid origin" error**: Check that your Gitea Mirror URL matches the configured redirect URI
|
1. **"Invalid origin" error**: Check that your Gitea Mirror URL matches the configured redirect URI
|
||||||
2. **"Provider not found" error**: Ensure the provider is properly configured and enabled
|
2. **"Provider not found" error**: Ensure the provider is properly configured and enabled
|
||||||
3. **Redirect loop**: Verify the redirect URI in both Gitea Mirror and the SSO provider match exactly
|
3. **Redirect loop**: Verify the redirect URI in both Gitea Mirror and the SSO provider match exactly
|
||||||
|
4. **`TypeError: undefined is not an object (evaluating 'url.startsWith')`**: This indicates the stored provider configuration is missing OIDC endpoints. Delete the provider from Gitea Mirror and re-register it using the **Discover** button so authorization/token URLs are saved (see [#73](https://github.com/RayLabsHQ/gitea-mirror/issues/73) and [#122](https://github.com/RayLabsHQ/gitea-mirror/issues/122) for examples).
|
||||||
|
|
||||||
### OIDC Provider Issues
|
### OIDC Provider Issues
|
||||||
|
|
||||||
@@ -202,4 +223,4 @@ This immediately prevents the application from authenticating new users.
|
|||||||
If migrating from the previous JWT-based authentication:
|
If migrating from the previous JWT-based authentication:
|
||||||
- Existing users remain unaffected
|
- Existing users remain unaffected
|
||||||
- Users can continue using email/password authentication
|
- Users can continue using email/password authentication
|
||||||
- SSO can be added as an additional authentication method
|
- SSO can be added as an additional authentication method
|
||||||
|
|||||||
127
docs/testing.md
@@ -1,127 +0,0 @@
|
|||||||
# Testing in Gitea Mirror
|
|
||||||
|
|
||||||
This document provides guidance on testing in the Gitea Mirror project.
|
|
||||||
|
|
||||||
## Current Status
|
|
||||||
|
|
||||||
The project now uses Bun's built-in test runner, which is Jest-compatible and provides a fast, reliable testing experience. We've migrated away from Vitest due to compatibility issues with Bun.
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
To run tests, use the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
bun test
|
|
||||||
|
|
||||||
# Run tests in watch mode (automatically re-run when files change)
|
|
||||||
bun test --watch
|
|
||||||
|
|
||||||
# Run tests with coverage reporting
|
|
||||||
bun test --coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test File Naming Conventions
|
|
||||||
|
|
||||||
Bun's test runner automatically discovers test files that match the following patterns:
|
|
||||||
|
|
||||||
- `*.test.{js|jsx|ts|tsx}`
|
|
||||||
- `*_test.{js|jsx|ts|tsx}`
|
|
||||||
- `*.spec.{js|jsx|ts|tsx}`
|
|
||||||
- `*_spec.{js|jsx|ts|tsx}`
|
|
||||||
|
|
||||||
## Writing Tests
|
|
||||||
|
|
||||||
The project uses Bun's test runner with a Jest-compatible API. Here's an example test:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// example.test.ts
|
|
||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
|
|
||||||
describe("Example Test", () => {
|
|
||||||
test("should pass", () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing React Components
|
|
||||||
|
|
||||||
For testing React components, we use React Testing Library:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// component.test.tsx
|
|
||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import MyComponent from "../components/MyComponent";
|
|
||||||
|
|
||||||
describe("MyComponent", () => {
|
|
||||||
test("renders correctly", () => {
|
|
||||||
render(<MyComponent />);
|
|
||||||
expect(screen.getByText("Hello World")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Setup
|
|
||||||
|
|
||||||
The test setup is defined in `src/tests/setup.bun.ts` and includes:
|
|
||||||
|
|
||||||
- Automatic cleanup after each test
|
|
||||||
- Setup for any global test environment needs
|
|
||||||
|
|
||||||
## Mocking
|
|
||||||
|
|
||||||
Bun's test runner provides built-in mocking capabilities:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { test, expect, mock } from "bun:test";
|
|
||||||
|
|
||||||
// Create a mock function
|
|
||||||
const mockFn = mock(() => "mocked value");
|
|
||||||
|
|
||||||
test("mock function", () => {
|
|
||||||
const result = mockFn();
|
|
||||||
expect(result).toBe("mocked value");
|
|
||||||
expect(mockFn).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock a module
|
|
||||||
mock.module("./some-module", () => {
|
|
||||||
return {
|
|
||||||
someFunction: () => "mocked module function"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## CI Integration
|
|
||||||
|
|
||||||
The CI workflow has been updated to use Bun's test runner. Tests are automatically run as part of the CI pipeline.
|
|
||||||
|
|
||||||
## Test Coverage
|
|
||||||
|
|
||||||
To generate test coverage reports, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun test --coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
This will generate a coverage report in the `coverage` directory.
|
|
||||||
|
|
||||||
## Types of Tests
|
|
||||||
|
|
||||||
The project includes several types of tests:
|
|
||||||
|
|
||||||
1. **Unit Tests**: Testing individual functions and utilities
|
|
||||||
2. **API Tests**: Testing API endpoints
|
|
||||||
3. **Component Tests**: Testing React components
|
|
||||||
4. **Integration Tests**: Testing how components work together
|
|
||||||
|
|
||||||
## Future Improvements
|
|
||||||
|
|
||||||
When expanding the test suite, consider:
|
|
||||||
|
|
||||||
1. Adding more comprehensive API endpoint tests
|
|
||||||
2. Increasing component test coverage
|
|
||||||
3. Setting up end-to-end tests with a tool like Playwright
|
|
||||||
4. Adding performance tests for critical paths
|
|
||||||
@@ -1 +1,11 @@
|
|||||||
CREATE UNIQUE INDEX `uniq_repositories_user_full_name` ON `repositories` (`user_id`,`full_name`);
|
-- Step 1: Remove duplicate repositories, keeping the most recently updated one
|
||||||
|
-- This handles cases where users have duplicate entries from before the unique constraint
|
||||||
|
DELETE FROM repositories
|
||||||
|
WHERE rowid NOT IN (
|
||||||
|
SELECT MAX(rowid)
|
||||||
|
FROM repositories
|
||||||
|
GROUP BY user_id, full_name
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
-- Step 2: Now create the unique index safely
|
||||||
|
CREATE UNIQUE INDEX uniq_repositories_user_full_name ON repositories (user_id, full_name);
|
||||||
4
drizzle/0006_military_la_nuit.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE `accounts` ADD `id_token` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `accounts` ADD `access_token_expires_at` integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE `accounts` ADD `refresh_token_expires_at` integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE `accounts` ADD `scope` text;
|
||||||
18
drizzle/0007_whole_hellion.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
ALTER TABLE `organizations` ADD `normalized_name` text NOT NULL DEFAULT '';--> statement-breakpoint
|
||||||
|
UPDATE `organizations` SET `normalized_name` = lower(trim(`name`));--> statement-breakpoint
|
||||||
|
DELETE FROM `organizations`
|
||||||
|
WHERE rowid NOT IN (
|
||||||
|
SELECT MIN(rowid)
|
||||||
|
FROM `organizations`
|
||||||
|
GROUP BY `user_id`, `normalized_name`
|
||||||
|
);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `uniq_organizations_user_normalized_name` ON `organizations` (`user_id`,`normalized_name`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `repositories` ADD `normalized_full_name` text NOT NULL DEFAULT '';--> statement-breakpoint
|
||||||
|
UPDATE `repositories` SET `normalized_full_name` = lower(trim(`full_name`));--> statement-breakpoint
|
||||||
|
DELETE FROM `repositories`
|
||||||
|
WHERE rowid NOT IN (
|
||||||
|
SELECT MIN(rowid)
|
||||||
|
FROM `repositories`
|
||||||
|
GROUP BY `user_id`, `normalized_full_name`
|
||||||
|
);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `uniq_repositories_user_normalized_full_name` ON `repositories` (`user_id`,`normalized_full_name`);
|
||||||
1
drizzle/0008_serious_thena.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `repositories` ADD `metadata` text;
|
||||||
1969
drizzle/meta/0006_snapshot.json
Normal file
1999
drizzle/meta/0007_snapshot.json
Normal file
2006
drizzle/meta/0008_snapshot.json
Normal file
@@ -43,6 +43,27 @@
|
|||||||
"when": 1757786449446,
|
"when": 1757786449446,
|
||||||
"tag": "0005_polite_preak",
|
"tag": "0005_polite_preak",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1761483928546,
|
||||||
|
"tag": "0006_military_la_nuit",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1761534391115,
|
||||||
|
"tag": "0007_whole_hellion",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1761802056073,
|
||||||
|
"tag": "0008_serious_thena",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
21
helm/gitea-mirror/.yamllint
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
extends: default
|
||||||
|
|
||||||
|
ignore: |
|
||||||
|
.yamllint
|
||||||
|
node_modules
|
||||||
|
templates
|
||||||
|
unittests/bash
|
||||||
|
|
||||||
|
rules:
|
||||||
|
truthy:
|
||||||
|
allowed-values: ['true', 'false']
|
||||||
|
check-keys: False
|
||||||
|
level: error
|
||||||
|
line-length: disable
|
||||||
|
document-start: disable
|
||||||
|
comments:
|
||||||
|
min-spaces-from-content: 1
|
||||||
|
braces:
|
||||||
|
max-spaces-inside: 2
|
||||||
|
|
||||||
12
helm/gitea-mirror/Chart.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: gitea-mirror
|
||||||
|
description: Kubernetes helm chart for gitea-mirror
|
||||||
|
type: application
|
||||||
|
version: 0.0.1
|
||||||
|
appVersion: 3.7.2
|
||||||
|
icon: https://github.com/RayLabsHQ/gitea-mirror/blob/main/.github/assets/logo.png
|
||||||
|
keywords:
|
||||||
|
- git
|
||||||
|
- gitea
|
||||||
|
sources:
|
||||||
|
- https://github.com/RayLabsHQ/gitea-mirror
|
||||||
307
helm/gitea-mirror/README.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# gitea-mirror (Helm Chart)
|
||||||
|
|
||||||
|
Deploy **gitea-mirror** to Kubernetes using Helm. The chart packages a Deployment, Service, optional Ingress or Gateway API HTTPRoutes, ConfigMap and Secret, a PVC (optional), and an optional ServiceAccount.
|
||||||
|
|
||||||
|
- **Chart name:** `gitea-mirror`
|
||||||
|
- **Type:** `application`
|
||||||
|
- **App version:** `3.7.2` (default image tag, can be overridden)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Kubernetes 1.23+
|
||||||
|
- Helm 3.8+
|
||||||
|
- (Optional) Gateway API (v1) if you plan to use `route.*` HTTPRoutes, see https://github.com/kubernetes-sigs/gateway-api/
|
||||||
|
- (Optional) An Ingress controller if you plan to use `ingress.*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
From the repo root (chart path: `helm/gitea-mirror`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a namespace (optional)
|
||||||
|
kubectl create namespace gitea-mirror
|
||||||
|
|
||||||
|
# Install with minimal required secrets/values
|
||||||
|
helm upgrade --install gitea-mirror ./helm/gitea-mirror --namespace gitea-mirror --set "gitea-mirror.github.username=<your-gh-username>" --set "gitea-mirror.github.token=<your-gh-token>" --set "gitea-mirror.gitea.url=https://gitea.example.com" --set "gitea-mirror.gitea.token=<your-gitea-token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
The default Service is `ClusterIP` on port `4321`. You can expose it via Ingress or Gateway API; see below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
Standard Helm upgrade:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm upgrade gitea-mirror ./helm/gitea-mirror -n gitea-mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
If you change persistence settings or storage class, a rollout may require PVC recreation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uninstalling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm uninstall gitea-mirror -n gitea-mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
If you enabled persistence with a PVC the data may persist; delete the PVC manually if you want a clean slate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Global image & pod settings
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `image.registry` | string | `ghcr.io` | Container registry. |
|
||||||
|
| `image.repository` | string | `raylabshq/gitea-mirror` | Image repository. |
|
||||||
|
| `image.tag` | string | `""` | Image tag; when empty, uses the chart `appVersion` (`3.7.2`). |
|
||||||
|
| `image.pullPolicy` | string | `IfNotPresent` | K8s image pull policy. |
|
||||||
|
| `imagePullSecrets` | list | `[]` | Image pull secrets. |
|
||||||
|
| `podSecurityContext.runAsUser` | int | `1001` | UID. |
|
||||||
|
| `podSecurityContext.runAsGroup` | int | `1001` | GID. |
|
||||||
|
| `podSecurityContext.fsGroup` | int | `1001` | FS group. |
|
||||||
|
| `podSecurityContext.fsGroupChangePolicy` | string | `OnRootMismatch` | FS group change policy. |
|
||||||
|
| `nodeSelector` / `tolerations` / `affinity` / `topologySpreadConstraints` | — | — | Standard scheduling knobs. |
|
||||||
|
| `extraVolumes` / `extraVolumeMounts` | list | `[]` | Append custom volumes/mounts. |
|
||||||
|
| `priorityClassName` | string | `""` | Optional Pod priority class. |
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `deployment.port` | int | `4321` | Container port & named `http` port. |
|
||||||
|
| `deployment.strategy.type` | string | `Recreate` | Update strategy (`Recreate` or `RollingUpdate`). |
|
||||||
|
| `deployment.strategy.rollingUpdate.maxUnavailable/maxSurge` | string/int | — | Used when `type=RollingUpdate`. |
|
||||||
|
| `deployment.env` | list | `[]` | Extra environment variables. |
|
||||||
|
| `deployment.resources` | map | `{}` | CPU/memory requests & limits. |
|
||||||
|
| `deployment.terminationGracePeriodSeconds` | int | `60` | Grace period. |
|
||||||
|
| `livenessProbe.*` | — | enabled, `/api/health` | Liveness probe (HTTP GET to `/api/health`). |
|
||||||
|
| `readinessProbe.*` | — | enabled, `/api/health` | Readiness probe. |
|
||||||
|
| `startupProbe.*` | — | enabled, `/api/health` | Startup probe. |
|
||||||
|
|
||||||
|
> The Pod mounts a volume at `/app/data` (PVC or `emptyDir` depending on `persistence.enabled`).
|
||||||
|
|
||||||
|
### Service
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `service.type` | string | `ClusterIP` | Service type. |
|
||||||
|
| `service.port` | int | `4321` | Service port. |
|
||||||
|
| `service.clusterIP` | string | `None` | ClusterIP (only when `type=ClusterIP`). |
|
||||||
|
| `service.externalTrafficPolicy` | string | `""` | External traffic policy (LB). |
|
||||||
|
| `service.loadBalancerIP` | string | `""` | LoadBalancer IP. |
|
||||||
|
| `service.loadBalancerClass` | string | `""` | LoadBalancer class. |
|
||||||
|
| `service.annotations` / `service.labels` | map | `{}` | Extra metadata. |
|
||||||
|
|
||||||
|
### Ingress (optional)
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `ingress.enabled` | bool | `false` | Enable Ingress. |
|
||||||
|
| `ingress.className` | string | `""` | IngressClass name. |
|
||||||
|
| `ingress.hosts[0].host` | string | `mirror.example.com` | Hostname. |
|
||||||
|
| `ingress.tls` | list | `[]` | TLS blocks (secret name etc.). |
|
||||||
|
| `ingress.annotations` | map | `{}` | Controller-specific annotations. |
|
||||||
|
|
||||||
|
> The Ingress exposes `/` to the chart’s Service.
|
||||||
|
|
||||||
|
### Gateway API HTTPRoutes (optional)
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `route.enabled` | bool | `false` | Enable Gateway API HTTPRoutes. |
|
||||||
|
| `route.forceHTTPS` | bool | `true` | If true, create an HTTP route that redirects to HTTPS (301). |
|
||||||
|
| `route.domain` | list | `["mirror.example.com"]` | Hostnames. |
|
||||||
|
| `route.gateway` | string | `""` | Gateway name. |
|
||||||
|
| `route.gatewayNamespace` | string | `""` | Gateway namespace. |
|
||||||
|
| `route.http.gatewaySection` | string | `""` | SectionName for HTTP listener. |
|
||||||
|
| `route.https.gatewaySection` | string | `""` | SectionName for HTTPS listener. |
|
||||||
|
| `route.http.filters` / `route.https.filters` | list | `[]` | Additional filters. (Defaults add HSTS header on HTTPS.) |
|
||||||
|
|
||||||
|
### Persistence
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `persistence.enabled` | bool | `true` | Enable persistent storage. |
|
||||||
|
| `persistence.create` | bool | `true` | Create a PVC from the chart. |
|
||||||
|
| `persistence.claimName` | string | `gitea-mirror-storage` | PVC name. |
|
||||||
|
| `persistence.storageClass` | string | `""` | StorageClass to use. |
|
||||||
|
| `persistence.accessModes` | list | `["ReadWriteOnce"]` | Access modes. |
|
||||||
|
| `persistence.size` | string | `1Gi` | Requested size. |
|
||||||
|
| `persistence.volumeName` | string | `""` | Bind to existing PV by name (optional). |
|
||||||
|
| `persistence.annotations` | map | `{}` | PVC annotations. |
|
||||||
|
|
||||||
|
### ServiceAccount (optional)
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `serviceAccount.create` | bool | `false` | Create a ServiceAccount. |
|
||||||
|
| `serviceAccount.name` | string | `""` | SA name (defaults to release fullname). |
|
||||||
|
| `serviceAccount.automountServiceAccountToken` | bool | `false` | Automount token. |
|
||||||
|
| `serviceAccount.annotations` / `labels` | map | `{}` | Extra metadata. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Application configuration (`gitea-mirror.*`)
|
||||||
|
|
||||||
|
These values populate a **ConfigMap** (non-secret) and a **Secret** (for tokens and sensitive fields). Environment variables from both are consumed by the container.
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
| Key | Default | Mapped env |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gitea-mirror.nodeEnv` | `production` | `NODE_ENV` |
|
||||||
|
| `gitea-mirror.core.databaseUrl` | `file:data/gitea-mirror.db` | `DATABASE_URL` |
|
||||||
|
| `gitea-mirror.core.encryptionSecret` | `""` | `ENCRYPTION_SECRET` (Secret) |
|
||||||
|
| `gitea-mirror.core.betterAuthSecret` | `""` | `BETTER_AUTH_SECRET` |
|
||||||
|
| `gitea-mirror.core.betterAuthUrl` | `http://localhost:4321` | `BETTER_AUTH_URL` |
|
||||||
|
| `gitea-mirror.core.betterAuthTrustedOrigins` | `http://localhost:4321` | `BETTER_AUTH_TRUSTED_ORIGINS` |
|
||||||
|
|
||||||
|
### GitHub
|
||||||
|
|
||||||
|
| Key | Default | Mapped env |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gitea-mirror.github.username` | `""` | `GITHUB_USERNAME` |
|
||||||
|
| `gitea-mirror.github.token` | `""` | `GITHUB_TOKEN` (Secret) |
|
||||||
|
| `gitea-mirror.github.type` | `personal` | `GITHUB_TYPE` |
|
||||||
|
| `gitea-mirror.github.privateRepositories` | `true` | `PRIVATE_REPOSITORIES` |
|
||||||
|
| `gitea-mirror.github.skipForks` | `false` | `SKIP_FORKS` |
|
||||||
|
| `gitea-mirror.github.starredCodeOnly` | `false` | `SKIP_STARRED_ISSUES` |
|
||||||
|
| `gitea-mirror.github.mirrorStarred` | `false` | `MIRROR_STARRED` |
|
||||||
|
|
||||||
|
### Gitea
|
||||||
|
|
||||||
|
| Key | Default | Mapped env |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gitea-mirror.gitea.url` | `""` | `GITEA_URL` |
|
||||||
|
| `gitea-mirror.gitea.token` | `""` | `GITEA_TOKEN` (Secret) |
|
||||||
|
| `gitea-mirror.gitea.username` | `""` | `GITEA_USERNAME` |
|
||||||
|
| `gitea-mirror.gitea.organization` | `github-mirrors` | `GITEA_ORGANIZATION` |
|
||||||
|
| `gitea-mirror.gitea.visibility` | `public` | `GITEA_ORG_VISIBILITY` |
|
||||||
|
|
||||||
|
### Mirror options
|
||||||
|
|
||||||
|
| Key | Default | Mapped env |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gitea-mirror.mirror.releases` | `true` | `MIRROR_RELEASES` |
|
||||||
|
| `gitea-mirror.mirror.wiki` | `true` | `MIRROR_WIKI` |
|
||||||
|
| `gitea-mirror.mirror.metadata` | `true` | `MIRROR_METADATA` |
|
||||||
|
| `gitea-mirror.mirror.issues` | `true` | `MIRROR_ISSUES` |
|
||||||
|
| `gitea-mirror.mirror.pullRequests` | `true` | `MIRROR_PULL_REQUESTS` |
|
||||||
|
| `gitea-mirror.mirror.starred` | _(see note above)_ | `MIRROR_STARRED` |
|
||||||
|
|
||||||
|
### Automation & cleanup
|
||||||
|
|
||||||
|
| Key | Default | Mapped env |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gitea-mirror.automation.schedule_enabled` | `true` | `SCHEDULE_ENABLED` |
|
||||||
|
| `gitea-mirror.automation.schedule_interval` | `3600` | `SCHEDULE_INTERVAL` (seconds) |
|
||||||
|
| `gitea-mirror.cleanup.enabled` | `true` | `CLEANUP_ENABLED` |
|
||||||
|
| `gitea-mirror.cleanup.retentionDays` | `30` | `CLEANUP_RETENTION_DAYS` |
|
||||||
|
|
||||||
|
> **Secrets:** If you set `gitea-mirror.existingSecret` (name of an existing Secret), the chart will **not** create its own Secret and will reference yours instead. Otherwise it creates a Secret with `GITHUB_TOKEN`, `GITEA_TOKEN`, `ENCRYPTION_SECRET`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exposing the service
|
||||||
|
|
||||||
|
### Using Ingress
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "nginx"
|
||||||
|
hosts:
|
||||||
|
- host: mirror.example.com
|
||||||
|
tls:
|
||||||
|
- secretName: mirror-tls
|
||||||
|
hosts:
|
||||||
|
- mirror.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates an Ingress routing `/` to the service on port `4321`.
|
||||||
|
|
||||||
|
### Using Gateway API (HTTPRoute)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
route:
|
||||||
|
enabled: true
|
||||||
|
domain: ["mirror.example.com"]
|
||||||
|
gateway: "my-gateway"
|
||||||
|
gatewayNamespace: "gateway-system"
|
||||||
|
http:
|
||||||
|
gatewaySection: "http"
|
||||||
|
https:
|
||||||
|
gatewaySection: "https"
|
||||||
|
# Example extra filter already included by default: add HSTS header
|
||||||
|
```
|
||||||
|
|
||||||
|
If `forceHTTPS: true`, the chart emits an HTTP route that redirects to HTTPS with 301. An HTTPS route is always created when `route.enabled=true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Persistence & data
|
||||||
|
|
||||||
|
By default, the chart provisions a PVC named `gitea-mirror-storage` with `1Gi` and mounts it at `/app/data`. To use an existing PV or tune storage, adjust `persistence.*` in `values.yaml`. If you disable persistence, an `emptyDir` will be used instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment & health endpoints
|
||||||
|
|
||||||
|
The container listens on `PORT` (defaults to `deployment.port` = `4321`) and exposes `GET /api/health` for liveness/readiness/startup probes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Minimal (tokens via chart-managed Secret)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
gitea-mirror:
|
||||||
|
github:
|
||||||
|
username: "gitea-mirror"
|
||||||
|
token: "<gh-token>"
|
||||||
|
gitea:
|
||||||
|
url: "https://gitea.company.tld"
|
||||||
|
token: "<gitea-token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bring your own Secret
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
gitea-mirror:
|
||||||
|
existingSecret: "gitea-mirror-secrets"
|
||||||
|
github:
|
||||||
|
username: "gitea-mirror"
|
||||||
|
gitea:
|
||||||
|
url: "https://gitea.company.tld"
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `gitea-mirror-secrets` contains keys `GITHUB_TOKEN`, `GITEA_TOKEN`, `ENCRYPTION_SECRET`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Lint the chart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yamllint -c helm/gitea-mirror/.yamllint helm/gitea-mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
Tweak probes, resources, and scheduling as needed; see `values.yaml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This chart is part of the `RayLabsHQ/gitea-mirror` repository. See the repository for licensing details.
|
||||||
59
helm/gitea-mirror/templates/_helpers.tpl
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{- define "gitea-mirror.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "gitea-mirror.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride -}}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||||
|
{{- if contains $name .Release.Name -}}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "gitea-mirror.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "gitea-mirror.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "gitea-mirror.chart" . }}
|
||||||
|
app: {{ include "gitea-mirror.name" . }}
|
||||||
|
{{ include "gitea-mirror.selectorLabels" . }}
|
||||||
|
app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
|
||||||
|
version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "gitea-mirror.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "gitea-mirror.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
ServiceAccount name
|
||||||
|
*/}}
|
||||||
|
{{- define "gitea-mirror.serviceAccountName" -}}
|
||||||
|
{{ .Values.serviceAccount.name | default (include "gitea-mirror.fullname" .) }}
|
||||||
|
{{- end -}}
|
||||||
38
helm/gitea-mirror/templates/configmap.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{{- $gm := index .Values "gitea-mirror" -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
data:
|
||||||
|
NODE_ENV: {{ $gm.nodeEnv | quote }}
|
||||||
|
# Core configuration
|
||||||
|
DATABASE_URL: {{ $gm.core.databaseUrl | quote }}
|
||||||
|
BETTER_AUTH_SECRET: {{ $gm.core.betterAuthSecret | quote }}
|
||||||
|
BETTER_AUTH_URL: {{ $gm.core.betterAuthUrl | quote }}
|
||||||
|
BETTER_AUTH_TRUSTED_ORIGINS: {{ $gm.core.betterAuthTrustedOrigins | quote }}
|
||||||
|
# GitHub Config
|
||||||
|
GITHUB_USERNAME: {{ $gm.github.username | quote }}
|
||||||
|
GITHUB_TYPE: {{ $gm.github.type | quote }}
|
||||||
|
PRIVATE_REPOSITORIES: {{ $gm.github.privateRepositories | quote }}
|
||||||
|
MIRROR_STARRED: {{ $gm.github.mirrorStarred | quote }}
|
||||||
|
SKIP_FORKS: {{ $gm.github.skipForks | quote }}
|
||||||
|
SKIP_STARRED_ISSUES: {{ $gm.github.starredCodeOnly | quote }}
|
||||||
|
# Gitea Config
|
||||||
|
GITEA_URL: {{ $gm.gitea.url | quote }}
|
||||||
|
GITEA_USERNAME: {{ $gm.gitea.username | quote }}
|
||||||
|
GITEA_ORGANIZATION: {{ $gm.gitea.organization | quote }}
|
||||||
|
GITEA_ORG_VISIBILITY: {{ $gm.gitea.visibility | quote }}
|
||||||
|
# Mirror Options
|
||||||
|
MIRROR_RELEASES: {{ $gm.mirror.releases | quote }}
|
||||||
|
MIRROR_WIKI: {{ $gm.mirror.wiki | quote }}
|
||||||
|
MIRROR_METADATA: {{ $gm.mirror.metadata | quote }}
|
||||||
|
MIRROR_ISSUES: {{ $gm.mirror.issues | quote }}
|
||||||
|
MIRROR_PULL_REQUESTS: {{ $gm.mirror.pullRequests | quote }}
|
||||||
|
# Automation
|
||||||
|
SCHEDULE_ENABLED: {{ $gm.automation.schedule_enabled| quote }}
|
||||||
|
SCHEDULE_INTERVAL: {{ $gm.automation.schedule_interval | quote }}
|
||||||
|
# Cleanup
|
||||||
|
CLEANUP_ENABLED: {{ $gm.cleanup.enabled | quote }}
|
||||||
|
CLEANUP_RETENTION_DAYS: {{ $gm.cleanup.retentionDays | quote }}
|
||||||
143
helm/gitea-mirror/templates/deployment.yaml
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{{- $gm := index .Values "gitea-mirror" -}}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
{{- with .Values.deployment.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
{{- if .Values.deployment.labels }}
|
||||||
|
{{- toYaml .Values.deployment.labels | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: {{ .Values.deployment.strategy.type }}
|
||||||
|
{{- if eq .Values.deployment.strategy.type "RollingUpdate" }}
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: {{ .Values.deployment.strategy.rollingUpdate.maxUnavailable }}
|
||||||
|
maxSurge: {{ .Values.deployment.strategy.rollingUpdate.maxSurge }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "gitea-mirror.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 8 }}
|
||||||
|
{{- if .Values.deployment.labels }}
|
||||||
|
{{- toYaml .Values.deployment.labels | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if (or .Values.serviceAccount.create .Values.serviceAccount.name) }}
|
||||||
|
serviceAccountName: {{ include "gitea-mirror.serviceAccountName" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.priorityClassName }}
|
||||||
|
priorityClassName: "{{ .Values.priorityClassName }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.podSecurityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }}
|
||||||
|
containers:
|
||||||
|
- name: gitea-mirror
|
||||||
|
image: {{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default (printf "v%s" .Chart.AppVersion) }}
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
{{- if $gm.existingSecret }}
|
||||||
|
- secretRef:
|
||||||
|
name: {{ $gm.existingSecret }}
|
||||||
|
{{- else }}
|
||||||
|
- secretRef:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
{{- end }}
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "{{ .Values.deployment.port }}"
|
||||||
|
{{- if .Values.deployment.env }}
|
||||||
|
{{- toYaml .Values.deployment.env | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.deployment.port }}
|
||||||
|
{{- if .Values.livenessProbe.enabled }}
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: "http"
|
||||||
|
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
|
||||||
|
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
|
||||||
|
successThreshold: {{ .Values.livenessProbe.successThreshold }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.readinessProbe.enabled }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: "http"
|
||||||
|
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
|
||||||
|
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
|
||||||
|
successThreshold: {{ .Values.readinessProbe.successThreshold }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.startupProbe.enabled }}
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: "http"
|
||||||
|
initialDelaySeconds: {{ .Values.startupProbe.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.startupProbe.periodSeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.startupProbe.timeoutSeconds }}
|
||||||
|
failureThreshold: {{ .Values.startupProbe.failureThreshold }}
|
||||||
|
successThreshold: {{ .Values.startupProbe.successThreshold }}
|
||||||
|
{{- end }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data
|
||||||
|
{{- if .Values.extraVolumeMounts }}
|
||||||
|
{{- toYaml .Values.extraVolumeMounts | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.deployment.resources }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.deployment.resources | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.topologySpreadConstraints }}
|
||||||
|
topologySpreadConstraints:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ .Values.persistence.claimName }}
|
||||||
|
{{- else if not .Values.persistence.enabled }}
|
||||||
|
- name: data
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.extraVolumes }}
|
||||||
|
{{- toYaml .Values.extraVolumes | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
77
helm/gitea-mirror/templates/httproute.yaml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{{- if .Values.route.enabled }}
|
||||||
|
{{- if .Values.route.forceHTTPS }}
|
||||||
|
---
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}-http
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: {{ .Values.route.gateway }}
|
||||||
|
sectionName: {{ .Values.route.http.gatewaySection }}
|
||||||
|
namespace: {{ .Values.route.gatewayNamespace }}
|
||||||
|
hostnames: {{ .Values.route.domain }}
|
||||||
|
rules:
|
||||||
|
- filters:
|
||||||
|
- type: RequestRedirect
|
||||||
|
requestRedirect:
|
||||||
|
scheme: https
|
||||||
|
statusCode: 301
|
||||||
|
{{- with .Values.route.http.filters }}
|
||||||
|
{{ toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
---
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}-http
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: {{ .Values.route.gateway }}
|
||||||
|
sectionName: {{ .Values.route.http.gatewaySection }}
|
||||||
|
namespace: {{ .Values.route.gatewayNamespace }}
|
||||||
|
hostnames: {{ .Values.route.domain }}
|
||||||
|
rules:
|
||||||
|
- matches:
|
||||||
|
- path:
|
||||||
|
type: PathPrefix
|
||||||
|
value: /
|
||||||
|
backendRefs:
|
||||||
|
- name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
port: {{ .Values.service.port }}
|
||||||
|
{{- with .Values.route.http.filters }}
|
||||||
|
filters:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
---
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}-https
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: {{ .Values.route.gateway }}
|
||||||
|
sectionName: {{ .Values.route.https.gatewaySection }}
|
||||||
|
namespace: {{ .Values.route.gatewayNamespace }}
|
||||||
|
hostnames: {{ .Values.route.domain }}
|
||||||
|
rules:
|
||||||
|
- matches:
|
||||||
|
- path:
|
||||||
|
type: PathPrefix
|
||||||
|
value: /
|
||||||
|
backendRefs:
|
||||||
|
- name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
port: {{ .Values.service.port }}
|
||||||
|
{{- with .Values.route.https.filters }}
|
||||||
|
filters:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
40
helm/gitea-mirror/templates/ingress.yaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- . | toYaml | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ tpl . $ | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ tpl .host $ | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: {{ .path | default "/" }}
|
||||||
|
pathType: {{ .pathType | default "Prefix" }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "gitea-mirror.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
26
helm/gitea-mirror/templates/pvc.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{{- if and .Values.persistence.enabled .Values.persistence.create }}
|
||||||
|
{{- $gm := index .Values "gitea-mirror" -}}
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.persistence.claimName }}
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.persistence.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{ . | toYaml | indent 4}}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
|
||||||
|
{{- with .Values.persistence.storageClass }}
|
||||||
|
storageClassName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
volumeMode: Filesystem
|
||||||
|
{{- with .Values.persistence.volumeName }}
|
||||||
|
volumeName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size }}
|
||||||
|
{{- end }}
|
||||||
14
helm/gitea-mirror/templates/secret.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{{- $gm := index .Values "gitea-mirror" -}}
|
||||||
|
{{- if (empty $gm.existingSecret) }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
GITHUB_TOKEN: {{ $gm.github.token | quote }}
|
||||||
|
GITEA_TOKEN: {{ $gm.gitea.token | quote }}
|
||||||
|
ENCRYPTION_SECRET: {{ $gm.core.encryptionSecret | quote }}
|
||||||
|
{{- end }}
|
||||||
34
helm/gitea-mirror/templates/service.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
{{- if .Values.service.labels }}
|
||||||
|
{{- toYaml .Values.service.labels | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml .Values.service.annotations | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
{{- if eq .Values.service.type "LoadBalancer" }}
|
||||||
|
{{- if .Values.service.loadBalancerClass }}
|
||||||
|
loadBalancerClass: {{ .Values.service.loadBalancerClass }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .Values.service.loadBalancerIP }}
|
||||||
|
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.service.externalTrafficPolicy }}
|
||||||
|
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .Values.service.clusterIP (eq .Values.service.type "ClusterIP") }}
|
||||||
|
clusterIP: {{ .Values.service.clusterIP }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.service.port }}
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: http
|
||||||
|
selector:
|
||||||
|
{{- include "gitea-mirror.selectorLabels" . | nindent 4 }}
|
||||||
17
helm/gitea-mirror/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "gitea-mirror.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.labels }}
|
||||||
|
{{- . | toYaml | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- . | toYaml | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
151
helm/gitea-mirror/values.yaml
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
image:
|
||||||
|
registry: ghcr.io
|
||||||
|
repository: raylabshq/gitea-mirror
|
||||||
|
# Leave blank to use the Appversion tag
|
||||||
|
tag: ""
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
podSecurityContext:
|
||||||
|
runAsUser: 1001
|
||||||
|
runAsGroup: 1001
|
||||||
|
fsGroup: 1001
|
||||||
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: ""
|
||||||
|
annotations: {}
|
||||||
|
hosts:
|
||||||
|
- host: mirror.example.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - mirror.example.com
|
||||||
|
|
||||||
|
route:
|
||||||
|
enabled: false
|
||||||
|
forceHTTPS: true
|
||||||
|
domain: ["mirror.example.com"]
|
||||||
|
gateway: ""
|
||||||
|
gatewayNamespace: ""
|
||||||
|
http:
|
||||||
|
gatewaySection: ""
|
||||||
|
filters: []
|
||||||
|
https:
|
||||||
|
gatewaySection: ""
|
||||||
|
filters:
|
||||||
|
- type: ResponseHeaderModifier
|
||||||
|
responseHeaderModifier:
|
||||||
|
add:
|
||||||
|
- name: Strict-Transport-Security
|
||||||
|
value: "max-age=31536000; includeSubDomains; preload"
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 4321
|
||||||
|
clusterIP: None
|
||||||
|
annotations: {}
|
||||||
|
externalTrafficPolicy:
|
||||||
|
labels: {}
|
||||||
|
loadBalancerIP:
|
||||||
|
loadBalancerClass:
|
||||||
|
|
||||||
|
deployment:
|
||||||
|
port: 4321
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
env: []
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
labels: {}
|
||||||
|
annotations: {}
|
||||||
|
resources: {}
|
||||||
|
|
||||||
|
livenessProbe:
|
||||||
|
enabled: true
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
successThreshold: 1
|
||||||
|
readinessProbe:
|
||||||
|
enabled: true
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
successThreshold: 1
|
||||||
|
startupProbe:
|
||||||
|
enabled: true
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
successThreshold: 1
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
create: true
|
||||||
|
claimName: gitea-mirror-storage
|
||||||
|
storageClass: ""
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
size: 1Gi
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
|
nodeSelector: {}
|
||||||
|
tolerations: []
|
||||||
|
topologySpreadConstraints: []
|
||||||
|
extraVolumes: []
|
||||||
|
extraVolumeMounts: []
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
create: false
|
||||||
|
name: ""
|
||||||
|
annotations: {}
|
||||||
|
labels: {}
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
|
||||||
|
gitea-mirror:
|
||||||
|
existingSecret: ""
|
||||||
|
nodeEnv: production
|
||||||
|
core:
|
||||||
|
databaseUrl: file:data/gitea-mirror.db
|
||||||
|
encryptionSecret: ""
|
||||||
|
betterAuthSecret: ""
|
||||||
|
betterAuthUrl: "http://localhost:4321"
|
||||||
|
betterAuthTrustedOrigins: "http://localhost:4321"
|
||||||
|
|
||||||
|
github:
|
||||||
|
username: ""
|
||||||
|
token: ""
|
||||||
|
type: personal
|
||||||
|
privateRepositories: true
|
||||||
|
mirrorStarred: false
|
||||||
|
skipForks: false
|
||||||
|
starredCodeOnly: false
|
||||||
|
|
||||||
|
gitea:
|
||||||
|
url: ""
|
||||||
|
token: ""
|
||||||
|
username: ""
|
||||||
|
organization: "github-mirrors"
|
||||||
|
visibility: "public"
|
||||||
|
|
||||||
|
mirror:
|
||||||
|
releases: true
|
||||||
|
wiki: true
|
||||||
|
metadata: true
|
||||||
|
issues: true
|
||||||
|
pullRequests: true
|
||||||
|
|
||||||
|
automation:
|
||||||
|
schedule_enabled: true
|
||||||
|
schedule_interval: 3600
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
retentionDays: 30
|
||||||
55
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.7.1",
|
"version": "3.9.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
"devalue": "^5.3.2"
|
"devalue": "^5.3.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.5",
|
||||||
"@astrojs/mdx": "4.3.5",
|
"@astrojs/mdx": "4.3.7",
|
||||||
"@astrojs/node": "9.4.3",
|
"@astrojs/node": "9.5.0",
|
||||||
"@astrojs/react": "^4.3.1",
|
"@astrojs/react": "^4.4.0",
|
||||||
"@better-auth/sso": "^1.3.9",
|
"@better-auth/sso": "1.4.0-beta.12",
|
||||||
"@octokit/plugin-throttling": "^11.0.1",
|
"@octokit/plugin-throttling": "^11.0.2",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
@@ -67,48 +67,49 @@
|
|||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.15",
|
||||||
"@tanstack/react-virtual": "^3.13.12",
|
"@tanstack/react-virtual": "^3.13.12",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.2.2",
|
||||||
"astro": "^5.13.7",
|
"astro": "^5.14.8",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"better-auth": "^1.3.9",
|
"buffer": "^6.0.3",
|
||||||
|
"better-auth": "1.4.0-beta.13",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.6",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.546.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.15",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "^1.2.21",
|
"@types/bun": "^1.3.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.5",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.6",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.2.21"
|
"packageManager": "bun@1.2.23"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3
public/favicon.svg
Normal file
|
After Width: | Height: | Size: 216 KiB |
@@ -114,10 +114,10 @@ EOF
|
|||||||
echo "======================================"
|
echo "======================================"
|
||||||
echo "1. Access Authentik at http://localhost:9000"
|
echo "1. Access Authentik at http://localhost:9000"
|
||||||
echo "2. Login with akadmin / admin-password"
|
echo "2. Login with akadmin / admin-password"
|
||||||
echo "3. Create OAuth2 Provider for Gitea Mirror:"
|
echo "3. Create an Authentik OIDC Provider for Gitea Mirror:"
|
||||||
echo " - Name: gitea-mirror"
|
echo " - Name: gitea-mirror"
|
||||||
echo " - Redirect URIs:"
|
echo " - Redirect URI:"
|
||||||
echo " http://localhost:4321/api/auth/callback/sso-provider"
|
echo " http://localhost:4321/api/auth/sso/callback/authentik"
|
||||||
echo " - Scopes: openid, profile, email"
|
echo " - Scopes: openid, profile, email"
|
||||||
echo ""
|
echo ""
|
||||||
echo "4. Create Application:"
|
echo "4. Create Application:"
|
||||||
@@ -131,10 +131,14 @@ EOF
|
|||||||
echo "6. Configure SSO in Gitea Mirror:"
|
echo "6. Configure SSO in Gitea Mirror:"
|
||||||
echo " - Go to Settings → Authentication & SSO"
|
echo " - Go to Settings → Authentication & SSO"
|
||||||
echo " - Add provider with:"
|
echo " - Add provider with:"
|
||||||
|
echo " - Provider ID: authentik"
|
||||||
echo " - Issuer URL: http://localhost:9000/application/o/gitea-mirror/"
|
echo " - Issuer URL: http://localhost:9000/application/o/gitea-mirror/"
|
||||||
|
echo " - Click Discover to pull Authentik endpoints"
|
||||||
echo " - Client ID: (from Authentik provider)"
|
echo " - Client ID: (from Authentik provider)"
|
||||||
echo " - Client Secret: (from Authentik provider)"
|
echo " - Client Secret: (from Authentik provider)"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "If you previously registered this provider on a version earlier than v3.8.10, delete it and re-add it after upgrading to avoid missing endpoint data."
|
||||||
|
echo ""
|
||||||
;;
|
;;
|
||||||
|
|
||||||
stop)
|
stop)
|
||||||
@@ -177,4 +181,4 @@ EOF
|
|||||||
echo " status - Show service status"
|
echo " status - Show service status"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -67,21 +67,21 @@ export function AdvancedOptionsForm({
|
|||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="skip-starred-issues"
|
id="starred-code-only"
|
||||||
checked={config.skipStarredIssues}
|
checked={config.starredCodeOnly}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleChange("skipStarredIssues", Boolean(checked))
|
handleChange("starredCodeOnly", Boolean(checked))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="skip-starred-issues"
|
htmlFor="starred-code-only"
|
||||||
className="ml-2 text-sm select-none"
|
className="ml-2 text-sm select-none"
|
||||||
>
|
>
|
||||||
Don't fetch issues for starred repos
|
Code-only mode for starred repos
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground ml-6">
|
<p className="text-xs text-muted-foreground ml-6">
|
||||||
Skip mirroring issues and pull requests for starred repositories
|
Mirror only source code for starred repositories, skipping all metadata (issues, PRs, labels, milestones, wiki, releases)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
Database,
|
Database,
|
||||||
@@ -16,7 +17,8 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
Activity,
|
Activity,
|
||||||
Zap,
|
Zap,
|
||||||
Info
|
Info,
|
||||||
|
Archive,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -120,13 +122,13 @@ export function AutomationSettings({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Automatic Syncing Section */}
|
{/* Automatic Syncing Section */}
|
||||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||||
<RefreshCw className="h-4 w-4 text-primary" />
|
<RefreshCw className="h-4 w-4 text-primary" />
|
||||||
Automatic Syncing
|
Automatic Syncing
|
||||||
</h3>
|
</h3>
|
||||||
{isAutoSavingSchedule && (
|
{isAutoSavingSchedule && (
|
||||||
@@ -139,6 +141,7 @@ export function AutomationSettings({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="enable-auto-mirror"
|
id="enable-auto-mirror"
|
||||||
checked={scheduleConfig.enabled}
|
checked={scheduleConfig.enabled}
|
||||||
|
className="mt-1.25"
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
onScheduleChange({ ...scheduleConfig, enabled: !!checked })
|
onScheduleChange({ ...scheduleConfig, enabled: !!checked })
|
||||||
}
|
}
|
||||||
@@ -218,17 +221,17 @@ export function AutomationSettings({
|
|||||||
Enable automatic syncing to schedule periodic repository updates
|
Enable automatic syncing to schedule periodic repository updates
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Database Cleanup Section */}
|
{/* Database Cleanup Section */}
|
||||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||||
<Database className="h-4 w-4 text-primary" />
|
<Database className="h-4 w-4 text-primary" />
|
||||||
Database Maintenance
|
Database Maintenance
|
||||||
</h3>
|
</h3>
|
||||||
{isAutoSavingCleanup && (
|
{isAutoSavingCleanup && (
|
||||||
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
@@ -239,6 +242,7 @@ export function AutomationSettings({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="enable-auto-cleanup"
|
id="enable-auto-cleanup"
|
||||||
checked={cleanupConfig.enabled}
|
checked={cleanupConfig.enabled}
|
||||||
|
className="mt-1.25"
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
onCleanupChange({ ...cleanupConfig, enabled: !!checked })
|
onCleanupChange({ ...cleanupConfig, enabled: !!checked })
|
||||||
}
|
}
|
||||||
@@ -257,8 +261,8 @@ export function AutomationSettings({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cleanupConfig.enabled && (
|
{cleanupConfig.enabled && (
|
||||||
<div className="ml-6 space-y-3">
|
<div className="ml-6 space-y-5">
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<Label htmlFor="retention-period" className="text-sm flex items-center gap-2">
|
<Label htmlFor="retention-period" className="text-sm flex items-center gap-2">
|
||||||
Data retention period
|
Data retention period
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -275,35 +279,36 @@ export function AutomationSettings({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<div className="flex items-center gap-3 mt-1.5">
|
||||||
value={cleanupConfig.retentionDays.toString()}
|
<Select
|
||||||
onValueChange={(value) =>
|
value={cleanupConfig.retentionDays.toString()}
|
||||||
onCleanupChange({
|
onValueChange={(value) =>
|
||||||
...cleanupConfig,
|
onCleanupChange({
|
||||||
retentionDays: parseInt(value, 10),
|
...cleanupConfig,
|
||||||
})
|
retentionDays: parseInt(value, 10),
|
||||||
}
|
})
|
||||||
>
|
}
|
||||||
<SelectTrigger id="retention-period" className="mt-1.5">
|
>
|
||||||
<SelectValue />
|
<SelectTrigger id="retention-period" className="w-40">
|
||||||
</SelectTrigger>
|
<SelectValue />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{retentionPeriods.map((option) => (
|
<SelectContent>
|
||||||
<SelectItem
|
{retentionPeriods.map((option) => (
|
||||||
key={option.value}
|
<SelectItem
|
||||||
value={option.value.toString()}
|
key={option.value}
|
||||||
>
|
value={option.value.toString()}
|
||||||
{option.label}
|
>
|
||||||
</SelectItem>
|
{option.label}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
{cleanupConfig.enabled && (
|
</Select>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground">
|
||||||
Cleanup runs {getCleanupFrequencyText(cleanupConfig.retentionDays)}
|
Cleanup runs {getCleanupFrequencyText(cleanupConfig.retentionDays)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -334,13 +339,108 @@ export function AutomationSettings({
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Enable automatic cleanup to optimize database storage
|
Enable automatic cleanup to optimize database storage
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
|
|
||||||
|
{/* Repository Cleanup Section */}
|
||||||
|
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50 md:col-span-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Archive className="h-4 w-4 text-primary" />
|
||||||
|
Repository Cleanup (orphaned mirrors)
|
||||||
|
</h3>
|
||||||
|
{isAutoSavingCleanup && (
|
||||||
|
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="cleanup-handle-orphans"
|
||||||
|
checked={Boolean(cleanupConfig.deleteIfNotInGitHub)}
|
||||||
|
className="mt-1.25"
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onCleanupChange({
|
||||||
|
...cleanupConfig,
|
||||||
|
deleteIfNotInGitHub: Boolean(checked),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="cleanup-handle-orphans"
|
||||||
|
className="text-sm font-normal cursor-pointer"
|
||||||
|
>
|
||||||
|
Handle orphaned repositories automatically
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Keep your Gitea backups when GitHub repos disappear. Archive is the safest option—it preserves data and disables automatic syncs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cleanupConfig.deleteIfNotInGitHub && (
|
||||||
|
<div className="space-y-3 ml-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="cleanup-orphaned-action" className="text-sm font-medium">
|
||||||
|
Action for orphaned repositories
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={cleanupConfig.orphanedRepoAction ?? "archive"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onCleanupChange({
|
||||||
|
...cleanupConfig,
|
||||||
|
orphanedRepoAction: value as DatabaseCleanupConfig["orphanedRepoAction"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="cleanup-orphaned-action">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="archive">Archive (preserve data)</SelectItem>
|
||||||
|
<SelectItem value="skip">Skip (leave as-is)</SelectItem>
|
||||||
|
<SelectItem value="delete">Delete from Gitea</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Archive renames mirror backups with an <code>archived-</code> prefix and disables automatic syncs—use Manual Sync when you want to refresh.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label
|
||||||
|
htmlFor="cleanup-dry-run"
|
||||||
|
className="text-sm font-normal cursor-pointer"
|
||||||
|
>
|
||||||
|
Dry run (log only)
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground max-w-xl">
|
||||||
|
When enabled, cleanup logs the planned action without modifying repositories.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="cleanup-dry-run"
|
||||||
|
checked={Boolean(cleanupConfig.dryRun)}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onCleanupChange({
|
||||||
|
...cleanupConfig,
|
||||||
|
dryRun: Boolean(checked),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export function ConfigTabs() {
|
|||||||
cleanupConfig: {
|
cleanupConfig: {
|
||||||
enabled: false, // Don't set defaults here - will be loaded from API
|
enabled: false, // Don't set defaults here - will be loaded from API
|
||||||
retentionDays: 0, // Will be replaced with actual value from API
|
retentionDays: 0, // Will be replaced with actual value from API
|
||||||
|
deleteIfNotInGitHub: true,
|
||||||
|
orphanedRepoAction: "archive",
|
||||||
|
dryRun: false,
|
||||||
|
deleteFromGitea: false,
|
||||||
|
protectedRepos: [],
|
||||||
},
|
},
|
||||||
mirrorOptions: {
|
mirrorOptions: {
|
||||||
mirrorReleases: false,
|
mirrorReleases: false,
|
||||||
@@ -71,7 +76,7 @@ export function ConfigTabs() {
|
|||||||
},
|
},
|
||||||
advancedOptions: {
|
advancedOptions: {
|
||||||
skipForks: false,
|
skipForks: false,
|
||||||
skipStarredIssues: false,
|
starredCodeOnly: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Checkbox } from "../ui/checkbox";
|
|
||||||
import type { DatabaseCleanupConfig } from "@/types/config";
|
|
||||||
import { formatDate } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "../ui/select";
|
|
||||||
import { RefreshCw, Database } from "lucide-react";
|
|
||||||
|
|
||||||
interface DatabaseCleanupConfigFormProps {
|
|
||||||
config: DatabaseCleanupConfig;
|
|
||||||
setConfig: React.Dispatch<React.SetStateAction<DatabaseCleanupConfig>>;
|
|
||||||
onAutoSave?: (config: DatabaseCleanupConfig) => void;
|
|
||||||
isAutoSaving?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Helper to calculate cleanup interval in hours (should match backend logic)
|
|
||||||
function calculateCleanupInterval(retentionSeconds: number): number {
|
|
||||||
const retentionDays = retentionSeconds / (24 * 60 * 60);
|
|
||||||
if (retentionDays <= 1) {
|
|
||||||
return 6;
|
|
||||||
} else if (retentionDays <= 3) {
|
|
||||||
return 12;
|
|
||||||
} else if (retentionDays <= 7) {
|
|
||||||
return 24;
|
|
||||||
} else if (retentionDays <= 30) {
|
|
||||||
return 48;
|
|
||||||
} else {
|
|
||||||
return 168;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DatabaseCleanupConfigForm({
|
|
||||||
config,
|
|
||||||
setConfig,
|
|
||||||
onAutoSave,
|
|
||||||
isAutoSaving = false,
|
|
||||||
}: DatabaseCleanupConfigFormProps) {
|
|
||||||
// Optimistically update nextRun when enabled or retention changes
|
|
||||||
const handleChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
|
||||||
) => {
|
|
||||||
const { name, value, type } = e.target;
|
|
||||||
let newConfig = {
|
|
||||||
...config,
|
|
||||||
[name]: type === "checkbox" ? (e.target as HTMLInputElement).checked : value,
|
|
||||||
};
|
|
||||||
|
|
||||||
// If enabling or changing retention, recalculate nextRun
|
|
||||||
if (
|
|
||||||
(name === "enabled" && (e.target as HTMLInputElement).checked) ||
|
|
||||||
(name === "retentionDays" && config.enabled)
|
|
||||||
) {
|
|
||||||
const now = new Date();
|
|
||||||
const retentionSeconds =
|
|
||||||
name === "retentionDays"
|
|
||||||
? Number(value)
|
|
||||||
: Number(newConfig.retentionDays);
|
|
||||||
const intervalHours = calculateCleanupInterval(retentionSeconds);
|
|
||||||
const nextRun = new Date(now.getTime() + intervalHours * 60 * 60 * 1000);
|
|
||||||
newConfig = {
|
|
||||||
...newConfig,
|
|
||||||
nextRun,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// If disabling, clear nextRun
|
|
||||||
if (name === "enabled" && !(e.target as HTMLInputElement).checked) {
|
|
||||||
newConfig = {
|
|
||||||
...newConfig,
|
|
||||||
nextRun: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig(newConfig);
|
|
||||||
if (onAutoSave) {
|
|
||||||
onAutoSave(newConfig);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Predefined retention periods (in seconds, like schedule intervals)
|
|
||||||
const retentionOptions: { value: number; label: string }[] = [
|
|
||||||
{ value: 86400, label: "1 day" }, // 24 * 60 * 60
|
|
||||||
{ value: 259200, label: "3 days" }, // 3 * 24 * 60 * 60
|
|
||||||
{ value: 604800, label: "7 days" }, // 7 * 24 * 60 * 60
|
|
||||||
{ value: 1209600, label: "14 days" }, // 14 * 24 * 60 * 60
|
|
||||||
{ value: 2592000, label: "30 days" }, // 30 * 24 * 60 * 60
|
|
||||||
{ value: 5184000, label: "60 days" }, // 60 * 24 * 60 * 60
|
|
||||||
{ value: 7776000, label: "90 days" }, // 90 * 24 * 60 * 60
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="self-start">
|
|
||||||
<CardContent className="pt-6 relative">
|
|
||||||
{isAutoSaving && (
|
|
||||||
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
|
|
||||||
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
|
|
||||||
<span className="text-xs">Auto-saving...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-y-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="cleanup-enabled"
|
|
||||||
name="enabled"
|
|
||||||
checked={config.enabled}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
name: "enabled",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: Boolean(checked),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="cleanup-enabled"
|
|
||||||
className="select-none ml-2 block text-sm font-medium"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Database className="h-4 w-4" />
|
|
||||||
Enable Automatic Database Cleanup
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{config.enabled && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2">
|
|
||||||
Data Retention Period
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
name="retentionDays"
|
|
||||||
value={String(config.retentionDays)}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleChange({
|
|
||||||
target: { name: "retentionDays", value },
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
|
||||||
<SelectValue placeholder="Select retention period" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
|
||||||
{retentionOptions.map((option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={option.value}
|
|
||||||
value={option.value.toString()}
|
|
||||||
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Activities and events older than this period will be automatically deleted.
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 p-2 bg-muted/50 rounded-md">
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
<strong>Cleanup Frequency:</strong> The cleanup process runs automatically at optimal intervals:
|
|
||||||
shorter retention periods trigger more frequent cleanups, longer periods trigger less frequent cleanups.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-x-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-sm font-medium mb-1">Last Cleanup</label>
|
|
||||||
<div className="text-sm">
|
|
||||||
{config.lastRun ? formatDate(config.lastRun) : "Never"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{config.enabled && (
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-sm font-medium mb-1">Next Cleanup</label>
|
|
||||||
<div className="text-sm">
|
|
||||||
{config.nextRun
|
|
||||||
? formatDate(config.nextRun)
|
|
||||||
: config.enabled
|
|
||||||
? "Calculating..."
|
|
||||||
: "Never"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -89,10 +89,10 @@ export function GitHubMirrorSettings({
|
|||||||
// Calculate what content is included for starred repos
|
// Calculate what content is included for starred repos
|
||||||
const starredRepoContent = {
|
const starredRepoContent = {
|
||||||
code: true, // Always included
|
code: true, // Always included
|
||||||
releases: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorReleases,
|
releases: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorReleases,
|
||||||
issues: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
issues: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||||
pullRequests: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
pullRequests: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||||
wiki: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
wiki: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||||
};
|
};
|
||||||
|
|
||||||
const starredContentCount = Object.entries(starredRepoContent).filter(([key, value]) => key !== 'code' && value).length;
|
const starredContentCount = Object.entries(starredRepoContent).filter(([key, value]) => key !== 'code' && value).length;
|
||||||
@@ -168,7 +168,7 @@ export function GitHubMirrorSettings({
|
|||||||
className="h-8 text-xs font-normal min-w-[140px] md:min-w-[140px] justify-between"
|
className="h-8 text-xs font-normal min-w-[140px] md:min-w-[140px] justify-between"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{advancedOptions.skipStarredIssues ? (
|
{advancedOptions.starredCodeOnly ? (
|
||||||
"Code only"
|
"Code only"
|
||||||
) : starredContentCount === 0 ? (
|
) : starredContentCount === 0 ? (
|
||||||
"Code only"
|
"Code only"
|
||||||
@@ -206,8 +206,8 @@ export function GitHubMirrorSettings({
|
|||||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="starred-lightweight"
|
id="starred-lightweight"
|
||||||
checked={advancedOptions.skipStarredIssues}
|
checked={advancedOptions.starredCodeOnly}
|
||||||
onCheckedChange={(checked) => handleAdvancedChange('skipStarredIssues', !!checked)}
|
onCheckedChange={(checked) => handleAdvancedChange('starredCodeOnly', !!checked)}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor="starred-lightweight"
|
htmlFor="starred-lightweight"
|
||||||
@@ -222,7 +222,7 @@ export function GitHubMirrorSettings({
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!advancedOptions.skipStarredIssues && (
|
{!advancedOptions.starredCodeOnly && (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
|
||||||
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
|
||||||
import { Separator } from '../ui/separator';
|
|
||||||
import type { ScheduleConfig, DatabaseCleanupConfig } from '@/types/config';
|
|
||||||
|
|
||||||
interface ScheduleAndCleanupFormProps {
|
|
||||||
scheduleConfig: ScheduleConfig;
|
|
||||||
cleanupConfig: DatabaseCleanupConfig;
|
|
||||||
setScheduleConfig: (update: ScheduleConfig | ((prev: ScheduleConfig) => ScheduleConfig)) => void;
|
|
||||||
setCleanupConfig: (update: DatabaseCleanupConfig | ((prev: DatabaseCleanupConfig) => DatabaseCleanupConfig)) => void;
|
|
||||||
onAutoSaveSchedule?: (config: ScheduleConfig) => Promise<void>;
|
|
||||||
onAutoSaveCleanup?: (config: DatabaseCleanupConfig) => Promise<void>;
|
|
||||||
isAutoSavingSchedule?: boolean;
|
|
||||||
isAutoSavingCleanup?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScheduleAndCleanupForm({
|
|
||||||
scheduleConfig,
|
|
||||||
cleanupConfig,
|
|
||||||
setScheduleConfig,
|
|
||||||
setCleanupConfig,
|
|
||||||
onAutoSaveSchedule,
|
|
||||||
onAutoSaveCleanup,
|
|
||||||
isAutoSavingSchedule,
|
|
||||||
isAutoSavingCleanup,
|
|
||||||
}: ScheduleAndCleanupFormProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<ScheduleConfigForm
|
|
||||||
config={scheduleConfig}
|
|
||||||
setConfig={setScheduleConfig}
|
|
||||||
onAutoSave={onAutoSaveSchedule}
|
|
||||||
isAutoSaving={isAutoSavingSchedule}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<DatabaseCleanupConfigForm
|
|
||||||
config={cleanupConfig}
|
|
||||||
setConfig={setCleanupConfig}
|
|
||||||
onAutoSave={onAutoSaveCleanup}
|
|
||||||
isAutoSaving={isAutoSavingCleanup}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -306,7 +306,7 @@ export function Dashboard() {
|
|||||||
title="Repositories"
|
title="Repositories"
|
||||||
value={repoCount}
|
value={repoCount}
|
||||||
icon={<GitFork className="h-4 w-4" />}
|
icon={<GitFork className="h-4 w-4" />}
|
||||||
description="Total in mirror queue"
|
description="Total imported repositories"
|
||||||
/>
|
/>
|
||||||
<StatusCard
|
<StatusCard
|
||||||
title="Mirrored"
|
title="Mirrored"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,9 +20,11 @@ interface AddOrganizationDialogProps {
|
|||||||
onAddOrganization: ({
|
onAddOrganization: ({
|
||||||
org,
|
org,
|
||||||
role,
|
role,
|
||||||
|
force,
|
||||||
}: {
|
}: {
|
||||||
org: string;
|
org: string;
|
||||||
role: MembershipRole;
|
role: MembershipRole;
|
||||||
|
force?: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +38,14 @@ export default function AddOrganizationDialog({
|
|||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDialogOpen) {
|
||||||
|
setError("");
|
||||||
|
setOrg("");
|
||||||
|
setRole("member");
|
||||||
|
}
|
||||||
|
}, [isDialogOpen]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -54,7 +64,7 @@ export default function AddOrganizationDialog({
|
|||||||
setRole("member");
|
setRole("member");
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message || "Failed to add repository.");
|
setError(err?.message || "Failed to add organization.");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -139,7 +149,7 @@ export default function AddOrganizationDialog({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
"Add Repository"
|
"Add Organization"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, RefreshCw, FlipHorizontal, Filter } from "lucide-react";
|
import { Search, RefreshCw, FlipHorizontal, Filter, LoaderCircle, Trash2 } from "lucide-react";
|
||||||
import type { MirrorJob, Organization } from "@/lib/db/schema";
|
import type { MirrorJob, Organization } from "@/lib/db/schema";
|
||||||
import { OrganizationList } from "./OrganizationsList";
|
import { OrganizationList } from "./OrganizationsList";
|
||||||
import AddOrganizationDialog from "./AddOrganizationDialog";
|
import AddOrganizationDialog from "./AddOrganizationDialog";
|
||||||
@@ -37,6 +37,14 @@ import {
|
|||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
export function Organization() {
|
export function Organization() {
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
@@ -52,6 +60,15 @@ export function Organization() {
|
|||||||
status: "",
|
status: "",
|
||||||
});
|
});
|
||||||
const [loadingOrgIds, setLoadingOrgIds] = useState<Set<string>>(new Set()); // this is used when the api actions are performed
|
const [loadingOrgIds, setLoadingOrgIds] = useState<Set<string>>(new Set()); // this is used when the api actions are performed
|
||||||
|
const [duplicateOrgCandidate, setDuplicateOrgCandidate] = useState<{
|
||||||
|
org: string;
|
||||||
|
role: MembershipRole;
|
||||||
|
} | null>(null);
|
||||||
|
const [isDuplicateOrgDialogOpen, setIsDuplicateOrgDialogOpen] = useState(false);
|
||||||
|
const [isProcessingDuplicateOrg, setIsProcessingDuplicateOrg] = useState(false);
|
||||||
|
const [orgToDelete, setOrgToDelete] = useState<Organization | null>(null);
|
||||||
|
const [isDeleteOrgDialogOpen, setIsDeleteOrgDialogOpen] = useState(false);
|
||||||
|
const [isDeletingOrg, setIsDeletingOrg] = useState(false);
|
||||||
|
|
||||||
// Create a stable callback using useCallback
|
// Create a stable callback using useCallback
|
||||||
const handleNewMessage = useCallback((data: MirrorJob) => {
|
const handleNewMessage = useCallback((data: MirrorJob) => {
|
||||||
@@ -256,19 +273,45 @@ export function Organization() {
|
|||||||
const handleAddOrganization = async ({
|
const handleAddOrganization = async ({
|
||||||
org,
|
org,
|
||||||
role,
|
role,
|
||||||
|
force = false,
|
||||||
}: {
|
}: {
|
||||||
org: string;
|
org: string;
|
||||||
role: MembershipRole;
|
role: MembershipRole;
|
||||||
|
force?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
if (!user || !user.id) {
|
||||||
if (!user || !user.id) {
|
return;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
const trimmedOrg = org.trim();
|
||||||
|
const normalizedOrg = trimmedOrg.toLowerCase();
|
||||||
|
|
||||||
|
if (!trimmedOrg) {
|
||||||
|
toast.error("Please enter a valid organization name.");
|
||||||
|
throw new Error("Invalid organization name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
const alreadyExists = organizations.some(
|
||||||
|
(existing) => existing.name?.trim().toLowerCase() === normalizedOrg
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyExists) {
|
||||||
|
toast.warning("Organization already exists.");
|
||||||
|
setDuplicateOrgCandidate({ org: trimmedOrg, role });
|
||||||
|
setIsDuplicateOrgDialogOpen(true);
|
||||||
|
throw new Error("Organization already exists");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
const reqPayload: AddOrganizationApiRequest = {
|
const reqPayload: AddOrganizationApiRequest = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
org,
|
org: trimmedOrg,
|
||||||
role,
|
role,
|
||||||
|
force,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await apiRequest<AddOrganizationApiResponse>(
|
const response = await apiRequest<AddOrganizationApiResponse>(
|
||||||
@@ -280,25 +323,100 @@ export function Organization() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success(`Organization added successfully`);
|
const message = force
|
||||||
setOrganizations((prev) => [...prev, response.organization]);
|
? "Organization already exists; using existing entry."
|
||||||
|
: "Organization added successfully";
|
||||||
|
toast.success(message);
|
||||||
|
|
||||||
await fetchOrganizations();
|
await fetchOrganizations(false);
|
||||||
|
|
||||||
setFilter((prev) => ({
|
setFilter((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
searchTerm: org,
|
searchTerm: trimmedOrg,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
setIsDuplicateOrgDialogOpen(false);
|
||||||
|
setDuplicateOrgCandidate(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showErrorToast(response.error || "Error adding organization", toast);
|
showErrorToast(response.error || "Error adding organization", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error, toast);
|
showErrorToast(error, toast);
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConfirmDuplicateOrganization = async () => {
|
||||||
|
if (!duplicateOrgCandidate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessingDuplicateOrg(true);
|
||||||
|
try {
|
||||||
|
await handleAddOrganization({
|
||||||
|
org: duplicateOrgCandidate.org,
|
||||||
|
role: duplicateOrgCandidate.role,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setDuplicateOrgCandidate(null);
|
||||||
|
setIsDuplicateOrgDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
// Error already surfaced via toast
|
||||||
|
} finally {
|
||||||
|
setIsProcessingDuplicateOrg(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelDuplicateOrganization = () => {
|
||||||
|
setIsDuplicateOrgDialogOpen(false);
|
||||||
|
setDuplicateOrgCandidate(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequestDeleteOrganization = (orgId: string) => {
|
||||||
|
const org = organizations.find((item) => item.id === orgId);
|
||||||
|
if (!org) {
|
||||||
|
toast.error("Organization not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrgToDelete(org);
|
||||||
|
setIsDeleteOrgDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteOrganization = async () => {
|
||||||
|
if (!user || !user.id || !orgToDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeletingOrg(true);
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<{ success: boolean; error?: string }>(
|
||||||
|
`/organizations/${orgToDelete.id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(`Removed ${orgToDelete.name} from Gitea Mirror.`);
|
||||||
|
await fetchOrganizations(false);
|
||||||
|
} else {
|
||||||
|
showErrorToast(response.error || "Failed to delete organization", toast);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
} finally {
|
||||||
|
setIsDeletingOrg(false);
|
||||||
|
setIsDeleteOrgDialogOpen(false);
|
||||||
|
setOrgToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMirrorAllOrgs = async () => {
|
const handleMirrorAllOrgs = async () => {
|
||||||
try {
|
try {
|
||||||
if (!user || !user.id || organizations.length === 0) {
|
if (!user || !user.id || organizations.length === 0) {
|
||||||
@@ -711,6 +829,7 @@ export function Organization() {
|
|||||||
onMirror={handleMirrorOrg}
|
onMirror={handleMirrorOrg}
|
||||||
onIgnore={handleIgnoreOrg}
|
onIgnore={handleIgnoreOrg}
|
||||||
onAddOrganization={() => setIsDialogOpen(true)}
|
onAddOrganization={() => setIsDialogOpen(true)}
|
||||||
|
onDelete={handleRequestDeleteOrganization}
|
||||||
onRefresh={async () => {
|
onRefresh={async () => {
|
||||||
await fetchOrganizations(false);
|
await fetchOrganizations(false);
|
||||||
}}
|
}}
|
||||||
@@ -721,6 +840,68 @@ export function Organization() {
|
|||||||
isDialogOpen={isDialogOpen}
|
isDialogOpen={isDialogOpen}
|
||||||
setIsDialogOpen={setIsDialogOpen}
|
setIsDialogOpen={setIsDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Dialog open={isDuplicateOrgDialogOpen} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
handleCancelDuplicateOrganization();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Organization already exists</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{duplicateOrgCandidate?.org ?? "This organization"} is already synced in Gitea Mirror.
|
||||||
|
Continuing will reuse the existing entry without creating a duplicate. You can remove it later if needed.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleCancelDuplicateOrganization} disabled={isProcessingDuplicateOrg}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirmDuplicateOrganization} disabled={isProcessingDuplicateOrg}>
|
||||||
|
{isProcessingDuplicateOrg ? (
|
||||||
|
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Continue"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={isDeleteOrgDialogOpen} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setIsDeleteOrgDialogOpen(false);
|
||||||
|
setOrgToDelete(null);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Remove organization from Gitea Mirror?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{orgToDelete?.name ?? "This organization"} will be deleted from Gitea Mirror only. Nothing will be removed from Gitea; you will need to clean it up manually in Gitea if desired.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => {
|
||||||
|
setIsDeleteOrgDialogOpen(false);
|
||||||
|
setOrgToDelete(null);
|
||||||
|
}} disabled={isDeletingOrg}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeleteOrganization} disabled={isDeletingOrg}>
|
||||||
|
{isDeletingOrg ? (
|
||||||
|
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock, MoreVertical, Ban } from "lucide-react";
|
import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock, MoreVertical, Ban, Trash2 } from "lucide-react";
|
||||||
import { SiGithub, SiGitea } from "react-icons/si";
|
import { SiGithub, SiGitea } from "react-icons/si";
|
||||||
import type { Organization } from "@/lib/db/schema";
|
import type { Organization } from "@/lib/db/schema";
|
||||||
import type { FilterParams } from "@/types/filter";
|
import type { FilterParams } from "@/types/filter";
|
||||||
@@ -30,6 +30,7 @@ interface OrganizationListProps {
|
|||||||
loadingOrgIds: Set<string>;
|
loadingOrgIds: Set<string>;
|
||||||
onAddOrganization?: () => void;
|
onAddOrganization?: () => void;
|
||||||
onRefresh?: () => Promise<void>;
|
onRefresh?: () => Promise<void>;
|
||||||
|
onDelete?: (orgId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get status badge variant and icon
|
// Helper function to get status badge variant and icon
|
||||||
@@ -60,6 +61,7 @@ export function OrganizationList({
|
|||||||
loadingOrgIds,
|
loadingOrgIds,
|
||||||
onAddOrganization,
|
onAddOrganization,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
onDelete,
|
||||||
}: OrganizationListProps) {
|
}: OrganizationListProps) {
|
||||||
const { giteaConfig } = useGiteaConfig();
|
const { giteaConfig } = useGiteaConfig();
|
||||||
|
|
||||||
@@ -414,7 +416,7 @@ export function OrganizationList({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dropdown menu for additional actions */}
|
{/* Dropdown menu for additional actions */}
|
||||||
{org.status !== "ignored" && org.status !== "mirroring" && (
|
{org.status !== "mirroring" && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={isLoading} className="h-10 w-10">
|
<Button variant="ghost" size="icon" disabled={isLoading} className="h-10 w-10">
|
||||||
@@ -422,12 +424,26 @@ export function OrganizationList({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem
|
{org.status !== "ignored" && (
|
||||||
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })}
|
<DropdownMenuItem
|
||||||
>
|
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })}
|
||||||
<Ban className="h-4 w-4 mr-2" />
|
>
|
||||||
Ignore Organization
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
</DropdownMenuItem>
|
Ignore Organization
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<>
|
||||||
|
{org.status !== "ignored" && <DropdownMenuSeparator />}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
onClick={() => org.id && onDelete(org.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete from Mirror
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
@@ -561,7 +577,7 @@ export function OrganizationList({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dropdown menu for additional actions */}
|
{/* Dropdown menu for additional actions */}
|
||||||
{org.status !== "ignored" && org.status !== "mirroring" && (
|
{org.status !== "mirroring" && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={isLoading}>
|
<Button variant="ghost" size="icon" disabled={isLoading}>
|
||||||
@@ -569,12 +585,26 @@ export function OrganizationList({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem
|
{org.status !== "ignored" && (
|
||||||
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })}
|
<DropdownMenuItem
|
||||||
>
|
onClick={() => org.id && onIgnore && onIgnore({ orgId: org.id, ignore: true })}
|
||||||
<Ban className="h-4 w-4 mr-2" />
|
>
|
||||||
Ignore Organization
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
</DropdownMenuItem>
|
Ignore Organization
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<>
|
||||||
|
{org.status !== "ignored" && <DropdownMenuSeparator />}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
onClick={() => org.id && onDelete(org.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete from Mirror
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -17,9 +17,11 @@ interface AddRepositoryDialogProps {
|
|||||||
onAddRepository: ({
|
onAddRepository: ({
|
||||||
repo,
|
repo,
|
||||||
owner,
|
owner,
|
||||||
|
force,
|
||||||
}: {
|
}: {
|
||||||
repo: string;
|
repo: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
|
force?: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +35,14 @@ export default function AddRepositoryDialog({
|
|||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDialogOpen) {
|
||||||
|
setError("");
|
||||||
|
setRepo("");
|
||||||
|
setOwner("");
|
||||||
|
}
|
||||||
|
}, [isDialogOpen]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "../ui/select";
|
} from "../ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, RefreshCw, FlipHorizontal, RotateCcw, X, Filter, Ban, Check } from "lucide-react";
|
import { Search, RefreshCw, FlipHorizontal, RotateCcw, X, Filter, Ban, Check, LoaderCircle, Trash2 } from "lucide-react";
|
||||||
import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
|
import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -30,6 +30,14 @@ import {
|
|||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { useSSE } from "@/hooks/useSEE";
|
import { useSSE } from "@/hooks/useSEE";
|
||||||
import { useFilterParams } from "@/hooks/useFilterParams";
|
import { useFilterParams } from "@/hooks/useFilterParams";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -69,6 +77,15 @@ export default function Repository() {
|
|||||||
}, [setFilter]);
|
}, [setFilter]);
|
||||||
|
|
||||||
const [loadingRepoIds, setLoadingRepoIds] = useState<Set<string>>(new Set()); // this is used when the api actions are performed
|
const [loadingRepoIds, setLoadingRepoIds] = useState<Set<string>>(new Set()); // this is used when the api actions are performed
|
||||||
|
const [duplicateRepoCandidate, setDuplicateRepoCandidate] = useState<{
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [isDuplicateRepoDialogOpen, setIsDuplicateRepoDialogOpen] = useState(false);
|
||||||
|
const [isProcessingDuplicateRepo, setIsProcessingDuplicateRepo] = useState(false);
|
||||||
|
const [repoToDelete, setRepoToDelete] = useState<Repository | null>(null);
|
||||||
|
const [isDeleteRepoDialogOpen, setIsDeleteRepoDialogOpen] = useState(false);
|
||||||
|
const [isDeletingRepo, setIsDeletingRepo] = useState(false);
|
||||||
|
|
||||||
// Create a stable callback using useCallback
|
// Create a stable callback using useCallback
|
||||||
const handleNewMessage = useCallback((data: MirrorJob) => {
|
const handleNewMessage = useCallback((data: MirrorJob) => {
|
||||||
@@ -320,7 +337,7 @@ export default function Repository() {
|
|||||||
|
|
||||||
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||||
const eligibleRepos = selectedRepos.filter(
|
const eligibleRepos = selectedRepos.filter(
|
||||||
repo => repo.status === "mirrored" || repo.status === "synced"
|
repo => ["mirrored", "synced", "archived"].includes(repo.status)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (eligibleRepos.length === 0) {
|
if (eligibleRepos.length === 0) {
|
||||||
@@ -618,19 +635,45 @@ export default function Repository() {
|
|||||||
const handleAddRepository = async ({
|
const handleAddRepository = async ({
|
||||||
repo,
|
repo,
|
||||||
owner,
|
owner,
|
||||||
|
force = false,
|
||||||
}: {
|
}: {
|
||||||
repo: string;
|
repo: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
|
force?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
if (!user || !user.id) {
|
||||||
if (!user || !user.id) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
const trimmedRepo = repo.trim();
|
||||||
|
const trimmedOwner = owner.trim();
|
||||||
|
|
||||||
|
if (!trimmedRepo || !trimmedOwner) {
|
||||||
|
toast.error("Please provide both owner and repository name.");
|
||||||
|
throw new Error("Invalid repository details");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFullName = `${trimmedOwner}/${trimmedRepo}`.toLowerCase();
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
const duplicateRepo = repositories.find(
|
||||||
|
(existing) => existing.normalizedFullName?.toLowerCase() === normalizedFullName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicateRepo) {
|
||||||
|
toast.warning("Repository already exists.");
|
||||||
|
setDuplicateRepoCandidate({ repo: trimmedRepo, owner: trimmedOwner });
|
||||||
|
setIsDuplicateRepoDialogOpen(true);
|
||||||
|
throw new Error("Repository already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const reqPayload: AddRepositoriesApiRequest = {
|
const reqPayload: AddRepositoriesApiRequest = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
repo,
|
repo: trimmedRepo,
|
||||||
owner,
|
owner: trimmedOwner,
|
||||||
|
force,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await apiRequest<AddRepositoriesApiResponse>(
|
const response = await apiRequest<AddRepositoriesApiResponse>(
|
||||||
@@ -642,20 +685,28 @@ export default function Repository() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success(`Repository added successfully`);
|
const message = force
|
||||||
setRepositories((prevRepos) => [...prevRepos, response.repository]);
|
? "Repository already exists; metadata refreshed."
|
||||||
|
: "Repository added successfully";
|
||||||
|
toast.success(message);
|
||||||
|
|
||||||
await fetchRepositories(false); // Manual refresh after adding repository
|
await fetchRepositories(false);
|
||||||
|
|
||||||
setFilter((prev) => ({
|
setFilter((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
searchTerm: repo,
|
searchTerm: trimmedRepo,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
setDuplicateRepoCandidate(null);
|
||||||
|
setIsDuplicateRepoDialogOpen(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showErrorToast(response.error || "Error adding repository", toast);
|
showErrorToast(response.error || "Error adding repository", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error, toast);
|
showErrorToast(error, toast);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -673,6 +724,71 @@ export default function Repository() {
|
|||||||
)
|
)
|
||||||
).sort();
|
).sort();
|
||||||
|
|
||||||
|
const handleConfirmDuplicateRepository = async () => {
|
||||||
|
if (!duplicateRepoCandidate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessingDuplicateRepo(true);
|
||||||
|
try {
|
||||||
|
await handleAddRepository({
|
||||||
|
repo: duplicateRepoCandidate.repo,
|
||||||
|
owner: duplicateRepoCandidate.owner,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
// Error already shown
|
||||||
|
} finally {
|
||||||
|
setIsProcessingDuplicateRepo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelDuplicateRepository = () => {
|
||||||
|
setDuplicateRepoCandidate(null);
|
||||||
|
setIsDuplicateRepoDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequestDeleteRepository = (repoId: string) => {
|
||||||
|
const repo = repositories.find((item) => item.id === repoId);
|
||||||
|
if (!repo) {
|
||||||
|
toast.error("Repository not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRepoToDelete(repo);
|
||||||
|
setIsDeleteRepoDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteRepository = async () => {
|
||||||
|
if (!user || !user.id || !repoToDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeletingRepo(true);
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<{ success: boolean; error?: string }>(
|
||||||
|
`/repositories/${repoToDelete.id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(`Removed ${repoToDelete.fullName} from Gitea Mirror.`);
|
||||||
|
await fetchRepositories(false);
|
||||||
|
} else {
|
||||||
|
showErrorToast(response.error || "Failed to delete repository", toast);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
} finally {
|
||||||
|
setIsDeletingRepo(false);
|
||||||
|
setIsDeleteRepoDialogOpen(false);
|
||||||
|
setRepoToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Determine what actions are available for selected repositories
|
// Determine what actions are available for selected repositories
|
||||||
const getAvailableActions = () => {
|
const getAvailableActions = () => {
|
||||||
if (selectedRepoIds.size === 0) return [];
|
if (selectedRepoIds.size === 0) return [];
|
||||||
@@ -1198,6 +1314,7 @@ export default function Repository() {
|
|||||||
onRefresh={async () => {
|
onRefresh={async () => {
|
||||||
await fetchRepositories(false);
|
await fetchRepositories(false);
|
||||||
}}
|
}}
|
||||||
|
onDelete={handleRequestDeleteRepository}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1206,6 +1323,77 @@ export default function Repository() {
|
|||||||
isDialogOpen={isDialogOpen}
|
isDialogOpen={isDialogOpen}
|
||||||
setIsDialogOpen={setIsDialogOpen}
|
setIsDialogOpen={setIsDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isDuplicateRepoDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
handleCancelDuplicateRepository();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Repository already exists</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{duplicateRepoCandidate ? `${duplicateRepoCandidate.owner}/${duplicateRepoCandidate.repo}` : "This repository"} is already tracked in Gitea Mirror. Continuing will refresh the existing entry without creating a duplicate.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleCancelDuplicateRepository} disabled={isProcessingDuplicateRepo}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirmDuplicateRepository} disabled={isProcessingDuplicateRepo}>
|
||||||
|
{isProcessingDuplicateRepo ? (
|
||||||
|
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Continue"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isDeleteRepoDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setIsDeleteRepoDialogOpen(false);
|
||||||
|
setRepoToDelete(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Remove repository from Gitea Mirror?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{repoToDelete?.fullName ?? "This repository"} will be deleted from Gitea Mirror only. The mirror on Gitea will remain untouched; remove it manually in Gitea if needed.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteRepoDialogOpen(false);
|
||||||
|
setRepoToDelete(null);
|
||||||
|
}}
|
||||||
|
disabled={isDeletingRepo}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeleteRepository} disabled={isDeletingRepo}>
|
||||||
|
{isDeletingRepo ? (
|
||||||
|
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock, Ban, Check, ChevronDown } from "lucide-react";
|
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock, Ban, Check, ChevronDown, Trash2 } from "lucide-react";
|
||||||
import { SiGithub, SiGitea } from "react-icons/si";
|
import { SiGithub, SiGitea } from "react-icons/si";
|
||||||
import type { Repository } from "@/lib/db/schema";
|
import type { Repository } from "@/lib/db/schema";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ interface RepositoryTableProps {
|
|||||||
selectedRepoIds: Set<string>;
|
selectedRepoIds: Set<string>;
|
||||||
onSelectionChange: (selectedIds: Set<string>) => void;
|
onSelectionChange: (selectedIds: Set<string>) => void;
|
||||||
onRefresh?: () => Promise<void>;
|
onRefresh?: () => Promise<void>;
|
||||||
|
onDelete?: (repoId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RepositoryTable({
|
export default function RepositoryTable({
|
||||||
@@ -56,6 +58,7 @@ export default function RepositoryTable({
|
|||||||
selectedRepoIds,
|
selectedRepoIds,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
onDelete,
|
||||||
}: RepositoryTableProps) {
|
}: RepositoryTableProps) {
|
||||||
const tableParentRef = useRef<HTMLDivElement>(null);
|
const tableParentRef = useRef<HTMLDivElement>(null);
|
||||||
const { giteaConfig } = useGiteaConfig();
|
const { giteaConfig } = useGiteaConfig();
|
||||||
@@ -90,7 +93,7 @@ export default function RepositoryTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only provide Gitea links for repositories that have been or are being mirrored
|
// Only provide Gitea links for repositories that have been or are being mirrored
|
||||||
const validStatuses = ['mirroring', 'mirrored', 'syncing', 'synced'];
|
const validStatuses = ['mirroring', 'mirrored', 'syncing', 'synced', 'archived'];
|
||||||
if (!validStatuses.includes(repository.status)) {
|
if (!validStatuses.includes(repository.status)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -676,6 +679,7 @@ export default function RepositoryTable({
|
|||||||
onSync={() => onSync({ repoId: repo.id ?? "" })}
|
onSync={() => onSync({ repoId: repo.id ?? "" })}
|
||||||
onRetry={() => onRetry({ repoId: repo.id ?? "" })}
|
onRetry={() => onRetry({ repoId: repo.id ?? "" })}
|
||||||
onSkip={(skip) => onSkip({ repoId: repo.id ?? "", skip })}
|
onSkip={(skip) => onSkip({ repoId: repo.id ?? "", skip })}
|
||||||
|
onDelete={onDelete && repo.id ? () => onDelete(repo.id as string) : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Links */}
|
{/* Links */}
|
||||||
@@ -786,6 +790,7 @@ function RepoActionButton({
|
|||||||
onSync,
|
onSync,
|
||||||
onRetry,
|
onRetry,
|
||||||
onSkip,
|
onSkip,
|
||||||
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
repo: { id: string; status: string };
|
repo: { id: string; status: string };
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -793,6 +798,7 @@ function RepoActionButton({
|
|||||||
onSync: () => void;
|
onSync: () => void;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
onSkip: (skip: boolean) => void;
|
onSkip: (skip: boolean) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
}) {
|
}) {
|
||||||
// For ignored repos, show an "Include" action
|
// For ignored repos, show an "Include" action
|
||||||
if (repo.status === "ignored") {
|
if (repo.status === "ignored") {
|
||||||
@@ -820,8 +826,8 @@ function RepoActionButton({
|
|||||||
primaryLabel = "Retry";
|
primaryLabel = "Retry";
|
||||||
primaryIcon = <RotateCcw className="h-4 w-4" />;
|
primaryIcon = <RotateCcw className="h-4 w-4" />;
|
||||||
primaryOnClick = onRetry;
|
primaryOnClick = onRetry;
|
||||||
} else if (["mirrored", "synced", "syncing"].includes(repo.status)) {
|
} else if (["mirrored", "synced", "syncing", "archived"].includes(repo.status)) {
|
||||||
primaryLabel = "Sync";
|
primaryLabel = repo.status === "archived" ? "Manual Sync" : "Sync";
|
||||||
primaryIcon = <RefreshCw className="h-4 w-4" />;
|
primaryIcon = <RefreshCw className="h-4 w-4" />;
|
||||||
primaryOnClick = onSync;
|
primaryOnClick = onSync;
|
||||||
primaryDisabled ||= repo.status === "syncing";
|
primaryDisabled ||= repo.status === "syncing";
|
||||||
@@ -849,7 +855,7 @@ function RepoActionButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show primary action with dropdown for skip option
|
// Show primary action with dropdown for additional actions
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -886,7 +892,19 @@ function RepoActionButton({
|
|||||||
<Ban className="h-4 w-4 mr-2" />
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
Ignore Repository
|
Ignore Repository
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{onDelete && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete from Mirror
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "@/lib/polyfills/buffer";
|
||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
import { oidcClient } from "better-auth/client/plugins";
|
import { oidcClient } from "better-auth/client/plugins";
|
||||||
import { ssoClient } from "@better-auth/sso/client";
|
import { ssoClient } from "@better-auth/sso/client";
|
||||||
@@ -60,4 +61,4 @@ export type Session = BetterAuthSession & {
|
|||||||
};
|
};
|
||||||
export type AuthUser = BetterAuthUser & {
|
export type AuthUser = BetterAuthUser & {
|
||||||
username?: string | null;
|
username?: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -166,9 +166,11 @@ export const auth = betterAuth({
|
|||||||
defaultOverrideUserInfo: true,
|
defaultOverrideUserInfo: true,
|
||||||
// Allow implicit sign up for new users
|
// Allow implicit sign up for new users
|
||||||
disableImplicitSignUp: false,
|
disableImplicitSignUp: false,
|
||||||
|
// Trust email_verified claims from the upstream provider so we can link by matching email
|
||||||
|
trustEmailVerified: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export type for use in other parts of the app
|
// Export type for use in other parts of the app
|
||||||
export type Auth = typeof auth;
|
export type Auth = typeof auth;
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export const githubConfigSchema = z.object({
|
|||||||
starredReposOrg: z.string().optional(),
|
starredReposOrg: z.string().optional(),
|
||||||
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
|
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
|
||||||
defaultOrg: z.string().optional(),
|
defaultOrg: z.string().optional(),
|
||||||
skipStarredIssues: z.boolean().default(false),
|
starredCodeOnly: z.boolean().default(false),
|
||||||
|
skipStarredIssues: z.boolean().optional(), // Deprecated: kept for backward compatibility, use starredCodeOnly instead
|
||||||
starredDuplicateStrategy: z.enum(["suffix", "prefix", "owner-org"]).default("suffix").optional(),
|
starredDuplicateStrategy: z.enum(["suffix", "prefix", "owner-org"]).default("suffix").optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,6 +54,8 @@ export const giteaConfigSchema = z.object({
|
|||||||
.enum(["skip", "reference", "full-copy"])
|
.enum(["skip", "reference", "full-copy"])
|
||||||
.default("reference"),
|
.default("reference"),
|
||||||
// Mirror options
|
// Mirror options
|
||||||
|
issueConcurrency: z.number().int().min(1).default(3),
|
||||||
|
pullRequestConcurrency: z.number().int().min(1).default(5),
|
||||||
mirrorReleases: z.boolean().default(false),
|
mirrorReleases: z.boolean().default(false),
|
||||||
releaseLimit: z.number().default(10),
|
releaseLimit: z.number().default(10),
|
||||||
mirrorMetadata: z.boolean().default(false),
|
mirrorMetadata: z.boolean().default(false),
|
||||||
@@ -93,7 +96,7 @@ export const cleanupConfigSchema = z.object({
|
|||||||
deleteFromGitea: z.boolean().default(false),
|
deleteFromGitea: z.boolean().default(false),
|
||||||
deleteIfNotInGitHub: z.boolean().default(true),
|
deleteIfNotInGitHub: z.boolean().default(true),
|
||||||
protectedRepos: z.array(z.string()).default([]),
|
protectedRepos: z.array(z.string()).default([]),
|
||||||
dryRun: z.boolean().default(true),
|
dryRun: z.boolean().default(false),
|
||||||
orphanedRepoAction: z
|
orphanedRepoAction: z
|
||||||
.enum(["skip", "archive", "delete"])
|
.enum(["skip", "archive", "delete"])
|
||||||
.default("archive"),
|
.default("archive"),
|
||||||
@@ -124,6 +127,7 @@ export const repositorySchema = z.object({
|
|||||||
configId: z.string(),
|
configId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
fullName: z.string(),
|
fullName: z.string(),
|
||||||
|
normalizedFullName: z.string(),
|
||||||
url: z.url(),
|
url: z.url(),
|
||||||
cloneUrl: z.url(),
|
cloneUrl: z.url(),
|
||||||
owner: z.string(),
|
owner: z.string(),
|
||||||
@@ -160,6 +164,7 @@ export const repositorySchema = z.object({
|
|||||||
lastMirrored: z.coerce.date().optional().nullable(),
|
lastMirrored: z.coerce.date().optional().nullable(),
|
||||||
errorMessage: z.string().optional().nullable(),
|
errorMessage: z.string().optional().nullable(),
|
||||||
destinationOrg: z.string().optional().nullable(),
|
destinationOrg: z.string().optional().nullable(),
|
||||||
|
metadata: z.string().optional().nullable(), // JSON string for metadata sync state
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
updatedAt: z.coerce.date(),
|
updatedAt: z.coerce.date(),
|
||||||
});
|
});
|
||||||
@@ -206,8 +211,9 @@ export const organizationSchema = z.object({
|
|||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
configId: z.string(),
|
configId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
normalizedName: z.string(),
|
||||||
avatarUrl: z.string(),
|
avatarUrl: z.string(),
|
||||||
membershipRole: z.enum(["admin", "member", "owner"]).default("member"),
|
membershipRole: z.enum(["member", "admin", "owner", "billing_manager"]).default("member"),
|
||||||
isIncluded: z.boolean().default(true),
|
isIncluded: z.boolean().default(true),
|
||||||
destinationOrg: z.string().optional().nullable(),
|
destinationOrg: z.string().optional().nullable(),
|
||||||
status: z
|
status: z
|
||||||
@@ -331,6 +337,7 @@ export const repositories = sqliteTable("repositories", {
|
|||||||
.references(() => configs.id),
|
.references(() => configs.id),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
fullName: text("full_name").notNull(),
|
fullName: text("full_name").notNull(),
|
||||||
|
normalizedFullName: text("normalized_full_name").notNull(),
|
||||||
url: text("url").notNull(),
|
url: text("url").notNull(),
|
||||||
cloneUrl: text("clone_url").notNull(),
|
cloneUrl: text("clone_url").notNull(),
|
||||||
owner: text("owner").notNull(),
|
owner: text("owner").notNull(),
|
||||||
@@ -370,6 +377,8 @@ export const repositories = sqliteTable("repositories", {
|
|||||||
|
|
||||||
destinationOrg: text("destination_org"),
|
destinationOrg: text("destination_org"),
|
||||||
|
|
||||||
|
metadata: text("metadata"), // JSON string storing metadata sync state (issues, PRs, releases, etc.)
|
||||||
|
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`(unixepoch())`),
|
.default(sql`(unixepoch())`),
|
||||||
@@ -385,6 +394,7 @@ export const repositories = sqliteTable("repositories", {
|
|||||||
index("idx_repositories_is_fork").on(table.isForked),
|
index("idx_repositories_is_fork").on(table.isForked),
|
||||||
index("idx_repositories_is_starred").on(table.isStarred),
|
index("idx_repositories_is_starred").on(table.isStarred),
|
||||||
uniqueIndex("uniq_repositories_user_full_name").on(table.userId, table.fullName),
|
uniqueIndex("uniq_repositories_user_full_name").on(table.userId, table.fullName),
|
||||||
|
uniqueIndex("uniq_repositories_user_normalized_full_name").on(table.userId, table.normalizedFullName),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const mirrorJobs = sqliteTable("mirror_jobs", {
|
export const mirrorJobs = sqliteTable("mirror_jobs", {
|
||||||
@@ -435,6 +445,7 @@ export const organizations = sqliteTable("organizations", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => configs.id),
|
.references(() => configs.id),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
|
normalizedName: text("normalized_name").notNull(),
|
||||||
|
|
||||||
avatarUrl: text("avatar_url").notNull(),
|
avatarUrl: text("avatar_url").notNull(),
|
||||||
|
|
||||||
@@ -466,6 +477,7 @@ export const organizations = sqliteTable("organizations", {
|
|||||||
index("idx_organizations_config_id").on(table.configId),
|
index("idx_organizations_config_id").on(table.configId),
|
||||||
index("idx_organizations_status").on(table.status),
|
index("idx_organizations_status").on(table.status),
|
||||||
index("idx_organizations_is_included").on(table.isIncluded),
|
index("idx_organizations_is_included").on(table.isIncluded),
|
||||||
|
uniqueIndex("uniq_organizations_user_normalized_name").on(table.userId, table.normalizedName),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ===== Better Auth Tables =====
|
// ===== Better Auth Tables =====
|
||||||
@@ -499,6 +511,10 @@ export const accounts = sqliteTable("accounts", {
|
|||||||
providerUserId: text("provider_user_id"), // Make nullable for email/password auth
|
providerUserId: text("provider_user_id"), // Make nullable for email/password auth
|
||||||
accessToken: text("access_token"),
|
accessToken: text("access_token"),
|
||||||
refreshToken: text("refresh_token"),
|
refreshToken: text("refresh_token"),
|
||||||
|
idToken: text("id_token"),
|
||||||
|
accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }),
|
||||||
|
refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
|
||||||
|
scope: text("scope"),
|
||||||
expiresAt: integer("expires_at", { mode: "timestamp" }),
|
expiresAt: integer("expires_at", { mode: "timestamp" }),
|
||||||
password: text("password"), // For credential provider
|
password: text("password"), // For credential provider
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface EnvConfig {
|
|||||||
mirrorOrganizations?: boolean;
|
mirrorOrganizations?: boolean;
|
||||||
preserveOrgStructure?: boolean;
|
preserveOrgStructure?: boolean;
|
||||||
onlyMirrorOrgs?: boolean;
|
onlyMirrorOrgs?: boolean;
|
||||||
skipStarredIssues?: boolean;
|
starredCodeOnly?: boolean;
|
||||||
starredReposOrg?: string;
|
starredReposOrg?: string;
|
||||||
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
|
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
|
||||||
};
|
};
|
||||||
@@ -49,6 +49,9 @@ interface EnvConfig {
|
|||||||
mirrorLabels?: boolean;
|
mirrorLabels?: boolean;
|
||||||
mirrorMilestones?: boolean;
|
mirrorMilestones?: boolean;
|
||||||
mirrorMetadata?: boolean;
|
mirrorMetadata?: boolean;
|
||||||
|
releaseLimit?: number;
|
||||||
|
issueConcurrency?: number;
|
||||||
|
pullRequestConcurrency?: number;
|
||||||
};
|
};
|
||||||
schedule: {
|
schedule: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -107,7 +110,7 @@ function parseEnvConfig(): EnvConfig {
|
|||||||
mirrorOrganizations: process.env.MIRROR_ORGANIZATIONS === 'true',
|
mirrorOrganizations: process.env.MIRROR_ORGANIZATIONS === 'true',
|
||||||
preserveOrgStructure: process.env.PRESERVE_ORG_STRUCTURE === 'true',
|
preserveOrgStructure: process.env.PRESERVE_ORG_STRUCTURE === 'true',
|
||||||
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
|
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
|
||||||
skipStarredIssues: process.env.SKIP_STARRED_ISSUES === 'true',
|
starredCodeOnly: process.env.SKIP_STARRED_ISSUES === 'true',
|
||||||
starredReposOrg: process.env.STARRED_REPOS_ORG,
|
starredReposOrg: process.env.STARRED_REPOS_ORG,
|
||||||
mirrorStrategy: process.env.MIRROR_STRATEGY as 'preserve' | 'single-org' | 'flat-user' | 'mixed',
|
mirrorStrategy: process.env.MIRROR_STRATEGY as 'preserve' | 'single-org' | 'flat-user' | 'mixed',
|
||||||
},
|
},
|
||||||
@@ -136,6 +139,8 @@ function parseEnvConfig(): EnvConfig {
|
|||||||
mirrorMilestones: process.env.MIRROR_MILESTONES === 'true',
|
mirrorMilestones: process.env.MIRROR_MILESTONES === 'true',
|
||||||
mirrorMetadata: process.env.MIRROR_METADATA === 'true',
|
mirrorMetadata: process.env.MIRROR_METADATA === 'true',
|
||||||
releaseLimit: process.env.RELEASE_LIMIT ? parseInt(process.env.RELEASE_LIMIT, 10) : undefined,
|
releaseLimit: process.env.RELEASE_LIMIT ? parseInt(process.env.RELEASE_LIMIT, 10) : undefined,
|
||||||
|
issueConcurrency: process.env.MIRROR_ISSUE_CONCURRENCY ? parseInt(process.env.MIRROR_ISSUE_CONCURRENCY, 10) : undefined,
|
||||||
|
pullRequestConcurrency: process.env.MIRROR_PULL_REQUEST_CONCURRENCY ? parseInt(process.env.MIRROR_PULL_REQUEST_CONCURRENCY, 10) : undefined,
|
||||||
},
|
},
|
||||||
schedule: {
|
schedule: {
|
||||||
enabled: process.env.SCHEDULE_ENABLED === 'true' ||
|
enabled: process.env.SCHEDULE_ENABLED === 'true' ||
|
||||||
@@ -169,7 +174,7 @@ function parseEnvConfig(): EnvConfig {
|
|||||||
deleteFromGitea: process.env.CLEANUP_DELETE_FROM_GITEA === 'true',
|
deleteFromGitea: process.env.CLEANUP_DELETE_FROM_GITEA === 'true',
|
||||||
deleteIfNotInGitHub: process.env.CLEANUP_DELETE_IF_NOT_IN_GITHUB === 'true',
|
deleteIfNotInGitHub: process.env.CLEANUP_DELETE_IF_NOT_IN_GITHUB === 'true',
|
||||||
protectedRepos,
|
protectedRepos,
|
||||||
dryRun: process.env.CLEANUP_DRY_RUN === 'true',
|
dryRun: process.env.CLEANUP_DRY_RUN === 'true' ? true : process.env.CLEANUP_DRY_RUN === 'false' ? false : false,
|
||||||
orphanedRepoAction: process.env.CLEANUP_ORPHANED_REPO_ACTION as 'skip' | 'archive' | 'delete',
|
orphanedRepoAction: process.env.CLEANUP_ORPHANED_REPO_ACTION as 'skip' | 'archive' | 'delete',
|
||||||
batchSize: process.env.CLEANUP_BATCH_SIZE ? parseInt(process.env.CLEANUP_BATCH_SIZE, 10) : undefined,
|
batchSize: process.env.CLEANUP_BATCH_SIZE ? parseInt(process.env.CLEANUP_BATCH_SIZE, 10) : undefined,
|
||||||
pauseBetweenDeletes: process.env.CLEANUP_PAUSE_BETWEEN_DELETES ? parseInt(process.env.CLEANUP_PAUSE_BETWEEN_DELETES, 10) : undefined,
|
pauseBetweenDeletes: process.env.CLEANUP_PAUSE_BETWEEN_DELETES ? parseInt(process.env.CLEANUP_PAUSE_BETWEEN_DELETES, 10) : undefined,
|
||||||
@@ -253,7 +258,7 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
|||||||
starredReposOrg: envConfig.github.starredReposOrg || existingConfig?.[0]?.githubConfig?.starredReposOrg || 'starred',
|
starredReposOrg: envConfig.github.starredReposOrg || existingConfig?.[0]?.githubConfig?.starredReposOrg || 'starred',
|
||||||
mirrorStrategy,
|
mirrorStrategy,
|
||||||
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
|
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
|
||||||
skipStarredIssues: envConfig.github.skipStarredIssues ?? existingConfig?.[0]?.githubConfig?.skipStarredIssues ?? false,
|
starredCodeOnly: envConfig.github.starredCodeOnly ?? existingConfig?.[0]?.githubConfig?.starredCodeOnly ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build Gitea config
|
// Build Gitea config
|
||||||
@@ -277,6 +282,12 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
|||||||
// Mirror metadata options
|
// Mirror metadata options
|
||||||
mirrorReleases: envConfig.mirror.mirrorReleases ?? existingConfig?.[0]?.giteaConfig?.mirrorReleases ?? false,
|
mirrorReleases: envConfig.mirror.mirrorReleases ?? existingConfig?.[0]?.giteaConfig?.mirrorReleases ?? false,
|
||||||
releaseLimit: envConfig.mirror.releaseLimit ?? existingConfig?.[0]?.giteaConfig?.releaseLimit ?? 10,
|
releaseLimit: envConfig.mirror.releaseLimit ?? existingConfig?.[0]?.giteaConfig?.releaseLimit ?? 10,
|
||||||
|
issueConcurrency: envConfig.mirror.issueConcurrency && envConfig.mirror.issueConcurrency > 0
|
||||||
|
? envConfig.mirror.issueConcurrency
|
||||||
|
: existingConfig?.[0]?.giteaConfig?.issueConcurrency ?? 3,
|
||||||
|
pullRequestConcurrency: envConfig.mirror.pullRequestConcurrency && envConfig.mirror.pullRequestConcurrency > 0
|
||||||
|
? envConfig.mirror.pullRequestConcurrency
|
||||||
|
: existingConfig?.[0]?.giteaConfig?.pullRequestConcurrency ?? 5,
|
||||||
mirrorMetadata: envConfig.mirror.mirrorMetadata ?? (envConfig.mirror.mirrorIssues || envConfig.mirror.mirrorPullRequests || envConfig.mirror.mirrorLabels || envConfig.mirror.mirrorMilestones) ?? existingConfig?.[0]?.giteaConfig?.mirrorMetadata ?? false,
|
mirrorMetadata: envConfig.mirror.mirrorMetadata ?? (envConfig.mirror.mirrorIssues || envConfig.mirror.mirrorPullRequests || envConfig.mirror.mirrorLabels || envConfig.mirror.mirrorMilestones) ?? existingConfig?.[0]?.giteaConfig?.mirrorMetadata ?? false,
|
||||||
mirrorIssues: envConfig.mirror.mirrorIssues ?? existingConfig?.[0]?.giteaConfig?.mirrorIssues ?? false,
|
mirrorIssues: envConfig.mirror.mirrorIssues ?? existingConfig?.[0]?.giteaConfig?.mirrorIssues ?? false,
|
||||||
mirrorPullRequests: envConfig.mirror.mirrorPullRequests ?? existingConfig?.[0]?.giteaConfig?.mirrorPullRequests ?? false,
|
mirrorPullRequests: envConfig.mirror.mirrorPullRequests ?? existingConfig?.[0]?.giteaConfig?.mirrorPullRequests ?? false,
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ mock.module("@/lib/helpers", () => ({
|
|||||||
createMirrorJob: mockCreateMirrorJob
|
createMirrorJob: mockCreateMirrorJob
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockMirrorGitHubReleasesToGitea = mock(() => Promise.resolve());
|
||||||
|
const mockMirrorGitRepoIssuesToGitea = mock(() => Promise.resolve());
|
||||||
|
const mockMirrorGitRepoPullRequestsToGitea = mock(() => Promise.resolve());
|
||||||
|
const mockMirrorGitRepoLabelsToGitea = mock(() => Promise.resolve());
|
||||||
|
const mockMirrorGitRepoMilestonesToGitea = mock(() => Promise.resolve());
|
||||||
|
const mockGetGiteaRepoOwnerAsync = mock(() => Promise.resolve("starred"));
|
||||||
|
|
||||||
// Mock the database module
|
// Mock the database module
|
||||||
const mockDb = {
|
const mockDb = {
|
||||||
insert: mock((table: any) => ({
|
insert: mock((table: any) => ({
|
||||||
@@ -125,6 +132,36 @@ const mockHttpGet = mock(async (url: string, headers?: any) => {
|
|||||||
headers: new Headers()
|
headers: new Headers()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (url.includes("/api/v1/repos/starred/metadata-repo")) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: 790,
|
||||||
|
name: "metadata-repo",
|
||||||
|
mirror: true,
|
||||||
|
owner: { login: "starred" },
|
||||||
|
mirror_interval: "8h",
|
||||||
|
private: false,
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: new Headers(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (url.includes("/api/v1/repos/starred/already-synced-repo")) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: 791,
|
||||||
|
name: "already-synced-repo",
|
||||||
|
mirror: true,
|
||||||
|
owner: { login: "starred" },
|
||||||
|
mirror_interval: "8h",
|
||||||
|
private: false,
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: new Headers(),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (url.includes("/api/v1/repos/")) {
|
if (url.includes("/api/v1/repos/")) {
|
||||||
throw new MockHttpError("Not Found", 404, "Not Found");
|
throw new MockHttpError("Not Found", 404, "Not Found");
|
||||||
}
|
}
|
||||||
@@ -220,6 +257,13 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
mockCreateMirrorJob.mockClear();
|
mockCreateMirrorJob.mockClear();
|
||||||
mockDb.insert.mockClear();
|
mockDb.insert.mockClear();
|
||||||
mockDb.update.mockClear();
|
mockDb.update.mockClear();
|
||||||
|
mockMirrorGitHubReleasesToGitea.mockClear();
|
||||||
|
mockMirrorGitRepoIssuesToGitea.mockClear();
|
||||||
|
mockMirrorGitRepoPullRequestsToGitea.mockClear();
|
||||||
|
mockMirrorGitRepoLabelsToGitea.mockClear();
|
||||||
|
mockMirrorGitRepoMilestonesToGitea.mockClear();
|
||||||
|
mockGetGiteaRepoOwnerAsync.mockClear();
|
||||||
|
mockGetGiteaRepoOwnerAsync.mockImplementation(() => Promise.resolve("starred"));
|
||||||
// Reset tracking variables
|
// Reset tracking variables
|
||||||
orgCheckCount = 0;
|
orgCheckCount = 0;
|
||||||
orgTestContext = "";
|
orgTestContext = "";
|
||||||
@@ -250,6 +294,7 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
url: "https://gitea.example.com",
|
url: "https://gitea.example.com",
|
||||||
token: "encrypted-token",
|
token: "encrypted-token",
|
||||||
defaultOwner: "testuser",
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -280,6 +325,7 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
url: "https://gitea.example.com",
|
url: "https://gitea.example.com",
|
||||||
token: "encrypted-token",
|
token: "encrypted-token",
|
||||||
defaultOwner: "testuser",
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -306,6 +352,7 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
url: "https://gitea.example.com",
|
url: "https://gitea.example.com",
|
||||||
token: "encrypted-token",
|
token: "encrypted-token",
|
||||||
defaultOwner: "testuser",
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -362,6 +409,7 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
url: "https://gitea.example.com",
|
url: "https://gitea.example.com",
|
||||||
token: "encrypted-token",
|
token: "encrypted-token",
|
||||||
defaultOwner: "testuser",
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -391,6 +439,7 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
url: "https://gitea.example.com",
|
url: "https://gitea.example.com",
|
||||||
token: "encrypted-token",
|
token: "encrypted-token",
|
||||||
defaultOwner: "testuser",
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,18 +458,21 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock getGiteaRepoOwnerAsync
|
|
||||||
const mockGetOwner = mock(() => Promise.resolve("starred"));
|
|
||||||
global.import = mock(async (path: string) => {
|
|
||||||
if (path === "./gitea") {
|
|
||||||
return { getGiteaRepoOwnerAsync: mockGetOwner };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}) as any;
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
syncGiteaRepoEnhanced({ config, repository })
|
syncGiteaRepoEnhanced(
|
||||||
|
{ config, repository },
|
||||||
|
{
|
||||||
|
getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync,
|
||||||
|
mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea,
|
||||||
|
mirrorGitRepoIssuesToGitea: mockMirrorGitRepoIssuesToGitea,
|
||||||
|
mirrorGitRepoPullRequestsToGitea: mockMirrorGitRepoPullRequestsToGitea,
|
||||||
|
mirrorGitRepoLabelsToGitea: mockMirrorGitRepoLabelsToGitea,
|
||||||
|
mirrorGitRepoMilestonesToGitea: mockMirrorGitRepoMilestonesToGitea,
|
||||||
|
}
|
||||||
|
)
|
||||||
).rejects.toThrow("Repository non-mirror-repo is not a mirror. Cannot sync.");
|
).rejects.toThrow("Repository non-mirror-repo is not a mirror. Cannot sync.");
|
||||||
|
|
||||||
|
expect(mockMirrorGitHubReleasesToGitea).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should successfully sync a mirror repository", async () => {
|
test("should successfully sync a mirror repository", async () => {
|
||||||
@@ -436,6 +488,7 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
url: "https://gitea.example.com",
|
url: "https://gitea.example.com",
|
||||||
token: "encrypted-token",
|
token: "encrypted-token",
|
||||||
defaultOwner: "testuser",
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -454,18 +507,150 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock getGiteaRepoOwnerAsync
|
const result = await syncGiteaRepoEnhanced(
|
||||||
const mockGetOwner = mock(() => Promise.resolve("starred"));
|
{ config, repository },
|
||||||
global.import = mock(async (path: string) => {
|
{
|
||||||
if (path === "./gitea") {
|
getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync,
|
||||||
return { getGiteaRepoOwnerAsync: mockGetOwner };
|
mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea,
|
||||||
|
mirrorGitRepoIssuesToGitea: mockMirrorGitRepoIssuesToGitea,
|
||||||
|
mirrorGitRepoPullRequestsToGitea: mockMirrorGitRepoPullRequestsToGitea,
|
||||||
|
mirrorGitRepoLabelsToGitea: mockMirrorGitRepoLabelsToGitea,
|
||||||
|
mirrorGitRepoMilestonesToGitea: mockMirrorGitRepoMilestonesToGitea,
|
||||||
}
|
}
|
||||||
return {};
|
);
|
||||||
}) as any;
|
|
||||||
|
|
||||||
const result = await syncGiteaRepoEnhanced({ config, repository });
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
expect(result).toEqual({ success: true });
|
||||||
|
expect(mockGetGiteaRepoOwnerAsync).toHaveBeenCalled();
|
||||||
|
expect(mockMirrorGitHubReleasesToGitea).toHaveBeenCalledTimes(1);
|
||||||
|
const releaseCall = mockMirrorGitHubReleasesToGitea.mock.calls[0][0];
|
||||||
|
expect(releaseCall.giteaOwner).toBe("starred");
|
||||||
|
expect(releaseCall.giteaRepoName).toBe("mirror-repo");
|
||||||
|
expect(releaseCall.config.githubConfig?.token).toBe("github-token");
|
||||||
|
expect(releaseCall.octokit).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mirrors metadata components when enabled and not previously synced", async () => {
|
||||||
|
const config: Partial<Config> = {
|
||||||
|
userId: "user123",
|
||||||
|
githubConfig: {
|
||||||
|
username: "testuser",
|
||||||
|
token: "github-token",
|
||||||
|
privateRepositories: true,
|
||||||
|
mirrorStarred: false,
|
||||||
|
},
|
||||||
|
giteaConfig: {
|
||||||
|
url: "https://gitea.example.com",
|
||||||
|
token: "encrypted-token",
|
||||||
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: true,
|
||||||
|
mirrorMetadata: true,
|
||||||
|
mirrorIssues: true,
|
||||||
|
mirrorPullRequests: true,
|
||||||
|
mirrorLabels: true,
|
||||||
|
mirrorMilestones: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const repository: Repository = {
|
||||||
|
id: "repo789",
|
||||||
|
name: "metadata-repo",
|
||||||
|
fullName: "user/metadata-repo",
|
||||||
|
owner: "user",
|
||||||
|
cloneUrl: "https://github.com/user/metadata-repo.git",
|
||||||
|
isPrivate: false,
|
||||||
|
isStarred: false,
|
||||||
|
status: repoStatusEnum.parse("mirrored"),
|
||||||
|
visibility: "public",
|
||||||
|
userId: "user123",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
metadata: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await syncGiteaRepoEnhanced(
|
||||||
|
{ config, repository },
|
||||||
|
{
|
||||||
|
getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync,
|
||||||
|
mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea,
|
||||||
|
mirrorGitRepoIssuesToGitea: mockMirrorGitRepoIssuesToGitea,
|
||||||
|
mirrorGitRepoPullRequestsToGitea: mockMirrorGitRepoPullRequestsToGitea,
|
||||||
|
mirrorGitRepoLabelsToGitea: mockMirrorGitRepoLabelsToGitea,
|
||||||
|
mirrorGitRepoMilestonesToGitea: mockMirrorGitRepoMilestonesToGitea,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockMirrorGitHubReleasesToGitea).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockMirrorGitRepoIssuesToGitea).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockMirrorGitRepoPullRequestsToGitea).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockMirrorGitRepoMilestonesToGitea).toHaveBeenCalledTimes(1);
|
||||||
|
// Labels should be skipped because issues already import them
|
||||||
|
expect(mockMirrorGitRepoLabelsToGitea).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips metadata mirroring when components already synced", async () => {
|
||||||
|
const config: Partial<Config> = {
|
||||||
|
userId: "user123",
|
||||||
|
githubConfig: {
|
||||||
|
username: "testuser",
|
||||||
|
token: "github-token",
|
||||||
|
privateRepositories: true,
|
||||||
|
mirrorStarred: false,
|
||||||
|
},
|
||||||
|
giteaConfig: {
|
||||||
|
url: "https://gitea.example.com",
|
||||||
|
token: "encrypted-token",
|
||||||
|
defaultOwner: "testuser",
|
||||||
|
mirrorReleases: false,
|
||||||
|
mirrorMetadata: true,
|
||||||
|
mirrorIssues: true,
|
||||||
|
mirrorPullRequests: true,
|
||||||
|
mirrorLabels: true,
|
||||||
|
mirrorMilestones: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const repository: Repository = {
|
||||||
|
id: "repo790",
|
||||||
|
name: "already-synced-repo",
|
||||||
|
fullName: "user/already-synced-repo",
|
||||||
|
owner: "user",
|
||||||
|
cloneUrl: "https://github.com/user/already-synced-repo.git",
|
||||||
|
isPrivate: false,
|
||||||
|
isStarred: false,
|
||||||
|
status: repoStatusEnum.parse("mirrored"),
|
||||||
|
visibility: "public",
|
||||||
|
userId: "user123",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
components: {
|
||||||
|
releases: true,
|
||||||
|
issues: true,
|
||||||
|
pullRequests: true,
|
||||||
|
labels: true,
|
||||||
|
milestones: true,
|
||||||
|
},
|
||||||
|
lastSyncedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await syncGiteaRepoEnhanced(
|
||||||
|
{ config, repository },
|
||||||
|
{
|
||||||
|
getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync,
|
||||||
|
mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea,
|
||||||
|
mirrorGitRepoIssuesToGitea: mockMirrorGitRepoIssuesToGitea,
|
||||||
|
mirrorGitRepoPullRequestsToGitea: mockMirrorGitRepoPullRequestsToGitea,
|
||||||
|
mirrorGitRepoLabelsToGitea: mockMirrorGitRepoLabelsToGitea,
|
||||||
|
mirrorGitRepoMilestonesToGitea: mockMirrorGitRepoMilestonesToGitea,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockMirrorGitHubReleasesToGitea).not.toHaveBeenCalled();
|
||||||
|
expect(mockMirrorGitRepoIssuesToGitea).not.toHaveBeenCalled();
|
||||||
|
expect(mockMirrorGitRepoPullRequestsToGitea).not.toHaveBeenCalled();
|
||||||
|
expect(mockMirrorGitRepoLabelsToGitea).not.toHaveBeenCalled();
|
||||||
|
expect(mockMirrorGitRepoMilestonesToGitea).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -567,4 +752,4 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
expect(deleteCalled).toBe(true);
|
expect(deleteCalled).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,26 @@
|
|||||||
|
|
||||||
import type { Config } from "@/types/config";
|
import type { Config } from "@/types/config";
|
||||||
import type { Repository } from "./db/schema";
|
import type { Repository } from "./db/schema";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
import { createMirrorJob } from "./helpers";
|
import { createMirrorJob } from "./helpers";
|
||||||
import { decryptConfigTokens } from "./utils/config-encryption";
|
import { decryptConfigTokens } from "./utils/config-encryption";
|
||||||
import { httpPost, httpGet, httpPatch, HttpError } from "./http-client";
|
import { httpPost, httpGet, httpPatch, HttpError } from "./http-client";
|
||||||
import { db, repositories } from "./db";
|
import { db, repositories } from "./db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { repoStatusEnum } from "@/types/Repository";
|
import { repoStatusEnum } from "@/types/Repository";
|
||||||
|
import {
|
||||||
|
parseRepositoryMetadataState,
|
||||||
|
serializeRepositoryMetadataState,
|
||||||
|
} from "./metadata-state";
|
||||||
|
|
||||||
|
type SyncDependencies = {
|
||||||
|
getGiteaRepoOwnerAsync: typeof import("./gitea")["getGiteaRepoOwnerAsync"];
|
||||||
|
mirrorGitHubReleasesToGitea: typeof import("./gitea")["mirrorGitHubReleasesToGitea"];
|
||||||
|
mirrorGitRepoIssuesToGitea: typeof import("./gitea")["mirrorGitRepoIssuesToGitea"];
|
||||||
|
mirrorGitRepoPullRequestsToGitea: typeof import("./gitea")["mirrorGitRepoPullRequestsToGitea"];
|
||||||
|
mirrorGitRepoLabelsToGitea: typeof import("./gitea")["mirrorGitRepoLabelsToGitea"];
|
||||||
|
mirrorGitRepoMilestonesToGitea: typeof import("./gitea")["mirrorGitRepoMilestonesToGitea"];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced repository information including mirror status
|
* Enhanced repository information including mirror status
|
||||||
@@ -239,7 +253,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
}: {
|
}: {
|
||||||
config: Partial<Config>;
|
config: Partial<Config>;
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
}): Promise<any> {
|
}, deps?: SyncDependencies): Promise<any> {
|
||||||
try {
|
try {
|
||||||
if (!config.userId || !config.giteaConfig?.url || !config.giteaConfig?.token) {
|
if (!config.userId || !config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||||
throw new Error("Gitea config is required.");
|
throw new Error("Gitea config is required.");
|
||||||
@@ -259,8 +273,8 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
.where(eq(repositories.id, repository.id!));
|
.where(eq(repositories.id, repository.id!));
|
||||||
|
|
||||||
// Get the expected owner
|
// Get the expected owner
|
||||||
const { getGiteaRepoOwnerAsync } = await import("./gitea");
|
const dependencies = deps ?? (await import("./gitea"));
|
||||||
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
const repoOwner = await dependencies.getGiteaRepoOwnerAsync({ config, repository });
|
||||||
|
|
||||||
// Check if repo exists and get its info
|
// Check if repo exists and get its info
|
||||||
const repoInfo = await getGiteaRepoInfo({
|
const repoInfo = await getGiteaRepoInfo({
|
||||||
@@ -324,6 +338,236 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const metadataState = parseRepositoryMetadataState(repository.metadata);
|
||||||
|
let metadataUpdated = false;
|
||||||
|
const skipMetadataForStarred =
|
||||||
|
repository.isStarred && config.githubConfig?.starredCodeOnly;
|
||||||
|
let metadataOctokit: Octokit | null = null;
|
||||||
|
|
||||||
|
const ensureOctokit = (): Octokit | null => {
|
||||||
|
if (metadataOctokit) {
|
||||||
|
return metadataOctokit;
|
||||||
|
}
|
||||||
|
if (!decryptedConfig.githubConfig?.token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
metadataOctokit = new Octokit({
|
||||||
|
auth: decryptedConfig.githubConfig.token,
|
||||||
|
});
|
||||||
|
return metadataOctokit;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldMirrorReleases =
|
||||||
|
!!config.giteaConfig?.mirrorReleases && !skipMetadataForStarred;
|
||||||
|
const shouldMirrorIssuesThisRun =
|
||||||
|
!!config.giteaConfig?.mirrorIssues &&
|
||||||
|
!skipMetadataForStarred &&
|
||||||
|
!metadataState.components.issues;
|
||||||
|
const shouldMirrorPullRequests =
|
||||||
|
!!config.giteaConfig?.mirrorPullRequests &&
|
||||||
|
!skipMetadataForStarred &&
|
||||||
|
!metadataState.components.pullRequests;
|
||||||
|
const shouldMirrorLabels =
|
||||||
|
!!config.giteaConfig?.mirrorLabels &&
|
||||||
|
!skipMetadataForStarred &&
|
||||||
|
!shouldMirrorIssuesThisRun &&
|
||||||
|
!metadataState.components.labels;
|
||||||
|
const shouldMirrorMilestones =
|
||||||
|
!!config.giteaConfig?.mirrorMilestones &&
|
||||||
|
!skipMetadataForStarred &&
|
||||||
|
!metadataState.components.milestones;
|
||||||
|
|
||||||
|
if (shouldMirrorReleases) {
|
||||||
|
const octokit = ensureOctokit();
|
||||||
|
if (!octokit) {
|
||||||
|
console.warn(
|
||||||
|
`[Sync] Skipping release mirroring for ${repository.name}: Missing GitHub token`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await dependencies.mirrorGitHubReleasesToGitea({
|
||||||
|
config,
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
|
giteaOwner: repoOwner,
|
||||||
|
giteaRepoName: repository.name,
|
||||||
|
});
|
||||||
|
metadataState.components.releases = true;
|
||||||
|
metadataUpdated = true;
|
||||||
|
console.log(
|
||||||
|
`[Sync] Mirrored releases for ${repository.name} after sync`
|
||||||
|
);
|
||||||
|
} catch (releaseError) {
|
||||||
|
console.error(
|
||||||
|
`[Sync] Failed to mirror releases for ${repository.name}: ${
|
||||||
|
releaseError instanceof Error
|
||||||
|
? releaseError.message
|
||||||
|
: String(releaseError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldMirrorIssuesThisRun) {
|
||||||
|
const octokit = ensureOctokit();
|
||||||
|
if (!octokit) {
|
||||||
|
console.warn(
|
||||||
|
`[Sync] Skipping issue mirroring for ${repository.name}: Missing GitHub token`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await dependencies.mirrorGitRepoIssuesToGitea({
|
||||||
|
config,
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
|
giteaOwner: repoOwner,
|
||||||
|
giteaRepoName: repository.name,
|
||||||
|
});
|
||||||
|
metadataState.components.issues = true;
|
||||||
|
metadataState.components.labels = true;
|
||||||
|
metadataUpdated = true;
|
||||||
|
console.log(
|
||||||
|
`[Sync] Mirrored issues for ${repository.name} after sync`
|
||||||
|
);
|
||||||
|
} catch (issueError) {
|
||||||
|
console.error(
|
||||||
|
`[Sync] Failed to mirror issues for ${repository.name}: ${
|
||||||
|
issueError instanceof Error
|
||||||
|
? issueError.message
|
||||||
|
: String(issueError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
config.giteaConfig?.mirrorIssues &&
|
||||||
|
metadataState.components.issues
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[Sync] Issues already mirrored for ${repository.name}; skipping`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldMirrorPullRequests) {
|
||||||
|
const octokit = ensureOctokit();
|
||||||
|
if (!octokit) {
|
||||||
|
console.warn(
|
||||||
|
`[Sync] Skipping pull request mirroring for ${repository.name}: Missing GitHub token`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await dependencies.mirrorGitRepoPullRequestsToGitea({
|
||||||
|
config,
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
|
giteaOwner: repoOwner,
|
||||||
|
giteaRepoName: repository.name,
|
||||||
|
});
|
||||||
|
metadataState.components.pullRequests = true;
|
||||||
|
metadataUpdated = true;
|
||||||
|
console.log(
|
||||||
|
`[Sync] Mirrored pull requests for ${repository.name} after sync`
|
||||||
|
);
|
||||||
|
} catch (prError) {
|
||||||
|
console.error(
|
||||||
|
`[Sync] Failed to mirror pull requests for ${repository.name}: ${
|
||||||
|
prError instanceof Error ? prError.message : String(prError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
config.giteaConfig?.mirrorPullRequests &&
|
||||||
|
metadataState.components.pullRequests
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[Sync] Pull requests already mirrored for ${repository.name}; skipping`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldMirrorLabels) {
|
||||||
|
const octokit = ensureOctokit();
|
||||||
|
if (!octokit) {
|
||||||
|
console.warn(
|
||||||
|
`[Sync] Skipping label mirroring for ${repository.name}: Missing GitHub token`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await dependencies.mirrorGitRepoLabelsToGitea({
|
||||||
|
config,
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
|
giteaOwner: repoOwner,
|
||||||
|
giteaRepoName: repository.name,
|
||||||
|
});
|
||||||
|
metadataState.components.labels = true;
|
||||||
|
metadataUpdated = true;
|
||||||
|
console.log(
|
||||||
|
`[Sync] Mirrored labels for ${repository.name} after sync`
|
||||||
|
);
|
||||||
|
} catch (labelError) {
|
||||||
|
console.error(
|
||||||
|
`[Sync] Failed to mirror labels for ${repository.name}: ${
|
||||||
|
labelError instanceof Error
|
||||||
|
? labelError.message
|
||||||
|
: String(labelError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
config.giteaConfig?.mirrorLabels &&
|
||||||
|
metadataState.components.labels
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[Sync] Labels already mirrored for ${repository.name}; skipping`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldMirrorMilestones) {
|
||||||
|
const octokit = ensureOctokit();
|
||||||
|
if (!octokit) {
|
||||||
|
console.warn(
|
||||||
|
`[Sync] Skipping milestone mirroring for ${repository.name}: Missing GitHub token`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await dependencies.mirrorGitRepoMilestonesToGitea({
|
||||||
|
config,
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
|
giteaOwner: repoOwner,
|
||||||
|
giteaRepoName: repository.name,
|
||||||
|
});
|
||||||
|
metadataState.components.milestones = true;
|
||||||
|
metadataUpdated = true;
|
||||||
|
console.log(
|
||||||
|
`[Sync] Mirrored milestones for ${repository.name} after sync`
|
||||||
|
);
|
||||||
|
} catch (milestoneError) {
|
||||||
|
console.error(
|
||||||
|
`[Sync] Failed to mirror milestones for ${repository.name}: ${
|
||||||
|
milestoneError instanceof Error
|
||||||
|
? milestoneError.message
|
||||||
|
: String(milestoneError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
config.giteaConfig?.mirrorMilestones &&
|
||||||
|
metadataState.components.milestones
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[Sync] Milestones already mirrored for ${repository.name}; skipping`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadataUpdated) {
|
||||||
|
metadataState.lastSyncedAt = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
// Mark repo as "synced" in DB
|
// Mark repo as "synced" in DB
|
||||||
await db
|
await db
|
||||||
.update(repositories)
|
.update(repositories)
|
||||||
@@ -333,6 +577,9 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
lastMirrored: new Date(),
|
lastMirrored: new Date(),
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
mirroredLocation: `${repoOwner}/${repository.name}`,
|
mirroredLocation: `${repoOwner}/${repository.name}`,
|
||||||
|
metadata: metadataUpdated
|
||||||
|
? serializeRepositoryMetadataState(metadataState)
|
||||||
|
: repository.metadata ?? null,
|
||||||
})
|
})
|
||||||
.where(eq(repositories.id, repository.id!));
|
.where(eq(repositories.id, repository.id!));
|
||||||
|
|
||||||
@@ -535,4 +782,4 @@ export async function handleExistingNonMirrorRepo({
|
|||||||
// TODO: Implement rename strategy if needed
|
// TODO: Implement rename strategy if needed
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,19 @@ import { createMockResponse, mockFetch } from "@/tests/mock-fetch";
|
|||||||
// Mock the isRepoPresentInGitea function
|
// Mock the isRepoPresentInGitea function
|
||||||
const mockIsRepoPresentInGitea = mock(() => Promise.resolve(false));
|
const mockIsRepoPresentInGitea = mock(() => Promise.resolve(false));
|
||||||
|
|
||||||
|
let mockDbSelectResult: any[] = [];
|
||||||
|
|
||||||
// Mock the database module
|
// Mock the database module
|
||||||
mock.module("@/lib/db", () => {
|
mock.module("@/lib/db", () => {
|
||||||
return {
|
return {
|
||||||
db: {
|
db: {
|
||||||
|
select: () => ({
|
||||||
|
from: () => ({
|
||||||
|
where: () => ({
|
||||||
|
limit: () => Promise.resolve(mockDbSelectResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
update: () => ({
|
update: () => ({
|
||||||
set: () => ({
|
set: () => ({
|
||||||
where: () => Promise.resolve()
|
where: () => Promise.resolve()
|
||||||
@@ -63,6 +72,7 @@ describe("Gitea Repository Mirroring", () => {
|
|||||||
originalConsoleError = console.error;
|
originalConsoleError = console.error;
|
||||||
console.log = mock(() => {});
|
console.log = mock(() => {});
|
||||||
console.error = mock(() => {});
|
console.error = mock(() => {});
|
||||||
|
mockDbSelectResult = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -331,7 +341,7 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
excludeOrgs: [],
|
excludeOrgs: [],
|
||||||
mirrorPublicOrgs: false,
|
mirrorPublicOrgs: false,
|
||||||
publicOrgs: [],
|
publicOrgs: [],
|
||||||
skipStarredIssues: false,
|
starredCodeOnly: false,
|
||||||
mirrorStrategy: "preserve"
|
mirrorStrategy: "preserve"
|
||||||
},
|
},
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
@@ -449,4 +459,37 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
const result = getGiteaRepoOwner({ config: configWithFlatUser, repository: repo });
|
const result = getGiteaRepoOwner({ config: configWithFlatUser, repository: repo });
|
||||||
expect(result).toBe("giteauser");
|
expect(result).toBe("giteauser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getGiteaRepoOwnerAsync honors organization override for owner role", async () => {
|
||||||
|
mockDbSelectResult = [
|
||||||
|
{
|
||||||
|
id: "org-id",
|
||||||
|
userId: "user-id",
|
||||||
|
configId: "config-id",
|
||||||
|
name: "myorg",
|
||||||
|
membershipRole: "owner",
|
||||||
|
status: "imported",
|
||||||
|
destinationOrg: "custom-org",
|
||||||
|
avatarUrl: "https://example.com/avatar.png",
|
||||||
|
isIncluded: true,
|
||||||
|
repositoryCount: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const configWithUser: Partial<Config> = {
|
||||||
|
...baseConfig,
|
||||||
|
userId: "user-id"
|
||||||
|
};
|
||||||
|
|
||||||
|
const repo = { ...baseRepo, organization: "myorg" };
|
||||||
|
|
||||||
|
const result = await getGiteaRepoOwnerAsync({
|
||||||
|
config: configWithUser,
|
||||||
|
repository: repo
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe("custom-org");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
810
src/lib/gitea.ts
75
src/lib/metadata-state.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
interface MetadataComponentsState {
|
||||||
|
releases: boolean;
|
||||||
|
issues: boolean;
|
||||||
|
pullRequests: boolean;
|
||||||
|
labels: boolean;
|
||||||
|
milestones: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepositoryMetadataState {
|
||||||
|
components: MetadataComponentsState;
|
||||||
|
lastSyncedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultComponents: MetadataComponentsState = {
|
||||||
|
releases: false,
|
||||||
|
issues: false,
|
||||||
|
pullRequests: false,
|
||||||
|
labels: false,
|
||||||
|
milestones: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDefaultMetadataState(): RepositoryMetadataState {
|
||||||
|
return {
|
||||||
|
components: { ...defaultComponents },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRepositoryMetadataState(
|
||||||
|
raw: unknown
|
||||||
|
): RepositoryMetadataState {
|
||||||
|
const base = createDefaultMetadataState();
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: any = raw;
|
||||||
|
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== "object") {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.components && typeof parsed.components === "object") {
|
||||||
|
base.components = {
|
||||||
|
...base.components,
|
||||||
|
releases: Boolean(parsed.components.releases),
|
||||||
|
issues: Boolean(parsed.components.issues),
|
||||||
|
pullRequests: Boolean(parsed.components.pullRequests),
|
||||||
|
labels: Boolean(parsed.components.labels),
|
||||||
|
milestones: Boolean(parsed.components.milestones),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof parsed.lastSyncedAt === "string") {
|
||||||
|
base.lastSyncedAt = parsed.lastSyncedAt;
|
||||||
|
} else if (typeof parsed.lastMetadataSync === "string") {
|
||||||
|
base.lastSyncedAt = parsed.lastMetadataSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeRepositoryMetadataState(
|
||||||
|
state: RepositoryMetadataState
|
||||||
|
): string {
|
||||||
|
return JSON.stringify(state);
|
||||||
|
}
|
||||||
5
src/lib/polyfills/buffer.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
|
if (typeof globalThis !== "undefined" && (globalThis as any).Buffer === undefined) {
|
||||||
|
(globalThis as any).Buffer = Buffer;
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ describe('normalizeGitRepoToInsert', () => {
|
|||||||
expect(insert.description).toBeNull();
|
expect(insert.description).toBeNull();
|
||||||
expect(insert.lastMirrored).toBeNull();
|
expect(insert.lastMirrored).toBeNull();
|
||||||
expect(insert.errorMessage).toBeNull();
|
expect(insert.errorMessage).toBeNull();
|
||||||
|
expect(insert.normalizedFullName).toBe(repo.fullName.toLowerCase());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,4 +73,3 @@ describe('calcBatchSizeForInsert', () => {
|
|||||||
expect(batch * 29).toBeLessThanOrEqual(999);
|
expect(batch * 29).toBeLessThanOrEqual(999);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function normalizeGitRepoToInsert(
|
|||||||
configId,
|
configId,
|
||||||
name: repo.name,
|
name: repo.name,
|
||||||
fullName: repo.fullName,
|
fullName: repo.fullName,
|
||||||
|
normalizedFullName: repo.fullName.toLowerCase(),
|
||||||
url: repo.url,
|
url: repo.url,
|
||||||
cloneUrl: repo.cloneUrl,
|
cloneUrl: repo.cloneUrl,
|
||||||
owner: repo.owner,
|
owner: repo.owner,
|
||||||
@@ -68,4 +69,3 @@ export function calcBatchSizeForInsert(columnCount: number, maxParams = 999): nu
|
|||||||
const effectiveMax = Math.max(1, maxParams - safety);
|
const effectiveMax = Math.max(1, maxParams - safety);
|
||||||
return Math.max(1, Math.floor(effectiveMax / columnCount));
|
return Math.max(1, Math.floor(effectiveMax / columnCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,20 @@ async function identifyOrphanedRepositories(config: any): Promise<any[]> {
|
|||||||
|
|
||||||
// Only identify repositories as orphaned if we successfully accessed GitHub
|
// Only identify repositories as orphaned if we successfully accessed GitHub
|
||||||
// This prevents false positives when GitHub is down or account is inaccessible
|
// This prevents false positives when GitHub is down or account is inaccessible
|
||||||
const orphanedRepos = dbRepos.filter(repo => !githubRepoFullNames.has(repo.fullName));
|
const orphanedRepos = dbRepos.filter(repo => {
|
||||||
|
const isOrphaned = !githubRepoFullNames.has(repo.fullName);
|
||||||
|
if (!isOrphaned) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip repositories we've already archived/preserved
|
||||||
|
if (repo.status === 'archived' || repo.isArchived) {
|
||||||
|
console.log(`[Repository Cleanup] Skipping ${repo.fullName} - already archived`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
if (orphanedRepos.length > 0) {
|
if (orphanedRepos.length > 0) {
|
||||||
console.log(`[Repository Cleanup] Found ${orphanedRepos.length} orphaned repositories for user ${userId}`);
|
console.log(`[Repository Cleanup] Found ${orphanedRepos.length} orphaned repositories for user ${userId}`);
|
||||||
@@ -98,7 +111,12 @@ async function handleOrphanedRepository(
|
|||||||
console.log(`[Repository Cleanup] Skipping orphaned repository ${repoFullName}`);
|
console.log(`[Repository Cleanup] Skipping orphaned repository ${repoFullName}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (repo.status === 'archived' || repo.isArchived) {
|
||||||
|
console.log(`[Repository Cleanup] Repository ${repoFullName} already archived; skipping additional actions`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
console.log(`[Repository Cleanup] DRY RUN: Would ${action} orphaned repository ${repoFullName}`);
|
console.log(`[Repository Cleanup] DRY RUN: Would ${action} orphaned repository ${repoFullName}`);
|
||||||
return;
|
return;
|
||||||
@@ -260,7 +278,7 @@ async function runRepositoryCleanup(config: any): Promise<{
|
|||||||
|
|
||||||
// Process orphaned repositories
|
// Process orphaned repositories
|
||||||
const action = cleanupConfig.orphanedRepoAction || 'archive';
|
const action = cleanupConfig.orphanedRepoAction || 'archive';
|
||||||
const dryRun = cleanupConfig.dryRun ?? true;
|
const dryRun = cleanupConfig.dryRun ?? false;
|
||||||
const batchSize = cleanupConfig.batchSize || 10;
|
const batchSize = cleanupConfig.batchSize || 10;
|
||||||
const pauseBetweenDeletes = cleanupConfig.pauseBetweenDeletes || 2000;
|
const pauseBetweenDeletes = cleanupConfig.pauseBetweenDeletes || 2000;
|
||||||
|
|
||||||
|
|||||||
@@ -99,12 +99,12 @@ async function runScheduledSync(config: any): Promise<void> {
|
|||||||
|
|
||||||
// Check for new repositories
|
// Check for new repositories
|
||||||
const existingRepos = await db
|
const existingRepos = await db
|
||||||
.select({ fullName: repositories.fullName })
|
.select({ normalizedFullName: repositories.normalizedFullName })
|
||||||
.from(repositories)
|
.from(repositories)
|
||||||
.where(eq(repositories.userId, userId));
|
.where(eq(repositories.userId, userId));
|
||||||
|
|
||||||
const existingRepoNames = new Set(existingRepos.map(r => r.fullName));
|
const existingRepoNames = new Set(existingRepos.map(r => r.normalizedFullName));
|
||||||
const newRepos = allGithubRepos.filter(r => !existingRepoNames.has(r.fullName));
|
const newRepos = allGithubRepos.filter(r => !existingRepoNames.has(r.fullName.toLowerCase()));
|
||||||
|
|
||||||
if (newRepos.length > 0) {
|
if (newRepos.length > 0) {
|
||||||
console.log(`[Scheduler] Found ${newRepos.length} new repositories for user ${userId}`);
|
console.log(`[Scheduler] Found ${newRepos.length} new repositories for user ${userId}`);
|
||||||
@@ -123,7 +123,7 @@ async function runScheduledSync(config: any): Promise<void> {
|
|||||||
await db
|
await db
|
||||||
.insert(repositories)
|
.insert(repositories)
|
||||||
.values(batch)
|
.values(batch)
|
||||||
.onConflictDoNothing({ target: [repositories.userId, repositories.fullName] });
|
.onConflictDoNothing({ target: [repositories.userId, repositories.normalizedFullName] });
|
||||||
}
|
}
|
||||||
console.log(`[Scheduler] Successfully imported ${newRepos.length} new repositories for user ${userId}`);
|
console.log(`[Scheduler] Successfully imported ${newRepos.length} new repositories for user ${userId}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -432,12 +432,12 @@ async function performInitialAutoStart(): Promise<void> {
|
|||||||
|
|
||||||
// Check for new repositories
|
// Check for new repositories
|
||||||
const existingRepos = await db
|
const existingRepos = await db
|
||||||
.select({ fullName: repositories.fullName })
|
.select({ normalizedFullName: repositories.normalizedFullName })
|
||||||
.from(repositories)
|
.from(repositories)
|
||||||
.where(eq(repositories.userId, config.userId));
|
.where(eq(repositories.userId, config.userId));
|
||||||
|
|
||||||
const existingRepoNames = new Set(existingRepos.map(r => r.fullName));
|
const existingRepoNames = new Set(existingRepos.map(r => r.normalizedFullName));
|
||||||
const reposToImport = allGithubRepos.filter(r => !existingRepoNames.has(r.fullName));
|
const reposToImport = allGithubRepos.filter(r => !existingRepoNames.has(r.fullName.toLowerCase()));
|
||||||
|
|
||||||
if (reposToImport.length > 0) {
|
if (reposToImport.length > 0) {
|
||||||
console.log(`[Scheduler] Importing ${reposToImport.length} repositories for user ${config.userId}...`);
|
console.log(`[Scheduler] Importing ${reposToImport.length} repositories for user ${config.userId}...`);
|
||||||
@@ -456,7 +456,7 @@ async function performInitialAutoStart(): Promise<void> {
|
|||||||
await db
|
await db
|
||||||
.insert(repositories)
|
.insert(repositories)
|
||||||
.values(batch)
|
.values(batch)
|
||||||
.onConflictDoNothing({ target: [repositories.userId, repositories.fullName] });
|
.onConflictDoNothing({ target: [repositories.userId, repositories.normalizedFullName] });
|
||||||
}
|
}
|
||||||
console.log(`[Scheduler] Successfully imported ${reposToImport.length} repositories`);
|
console.log(`[Scheduler] Successfully imported ${reposToImport.length} repositories`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
75
src/lib/sso/oidc-config.test.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { normalizeOidcProviderConfig, OidcConfigError } from "./oidc-config";
|
||||||
|
|
||||||
|
const issuer = "https://auth.example.com";
|
||||||
|
|
||||||
|
describe("normalizeOidcProviderConfig", () => {
|
||||||
|
it("returns provided endpoints when complete", async () => {
|
||||||
|
const result = await normalizeOidcProviderConfig(issuer, {
|
||||||
|
clientId: "client",
|
||||||
|
clientSecret: "secret",
|
||||||
|
authorizationEndpoint: "https://auth.example.com/auth",
|
||||||
|
tokenEndpoint: "https://auth.example.com/token",
|
||||||
|
jwksEndpoint: "https://auth.example.com/jwks",
|
||||||
|
userInfoEndpoint: "https://auth.example.com/userinfo",
|
||||||
|
scopes: ["openid", "email"],
|
||||||
|
pkce: false,
|
||||||
|
}, async () => {
|
||||||
|
throw new Error("fetch should not be called when endpoints are provided");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.oidcConfig.authorizationEndpoint).toBe("https://auth.example.com/auth");
|
||||||
|
expect(result.oidcConfig.tokenEndpoint).toBe("https://auth.example.com/token");
|
||||||
|
expect(result.oidcConfig.jwksEndpoint).toBe("https://auth.example.com/jwks");
|
||||||
|
expect(result.oidcConfig.userInfoEndpoint).toBe("https://auth.example.com/userinfo");
|
||||||
|
expect(result.oidcConfig.scopes).toEqual(["openid", "email"]);
|
||||||
|
expect(result.oidcConfig.pkce).toBe(false);
|
||||||
|
expect(result.oidcConfig.discoveryEndpoint).toBe("https://auth.example.com/.well-known/openid-configuration");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives missing fields from discovery", async () => {
|
||||||
|
const fetchMock = async () =>
|
||||||
|
new Response(JSON.stringify({
|
||||||
|
authorization_endpoint: "https://auth.example.com/auth",
|
||||||
|
token_endpoint: "https://auth.example.com/token",
|
||||||
|
jwks_uri: "https://auth.example.com/jwks",
|
||||||
|
userinfo_endpoint: "https://auth.example.com/userinfo",
|
||||||
|
scopes_supported: ["openid", "email", "profile"],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await normalizeOidcProviderConfig(issuer, {
|
||||||
|
clientId: "client",
|
||||||
|
clientSecret: "secret",
|
||||||
|
}, fetchMock);
|
||||||
|
|
||||||
|
expect(result.oidcConfig.authorizationEndpoint).toBe("https://auth.example.com/auth");
|
||||||
|
expect(result.oidcConfig.tokenEndpoint).toBe("https://auth.example.com/token");
|
||||||
|
expect(result.oidcConfig.jwksEndpoint).toBe("https://auth.example.com/jwks");
|
||||||
|
expect(result.oidcConfig.userInfoEndpoint).toBe("https://auth.example.com/userinfo");
|
||||||
|
expect(result.oidcConfig.scopes).toEqual(["openid", "email", "profile"]);
|
||||||
|
expect(result.oidcConfig.discoveryEndpoint).toBe("https://auth.example.com/.well-known/openid-configuration");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves trailing slash issuers when building discovery endpoints", async () => {
|
||||||
|
const trailingIssuer = "https://auth.example.com/application/o/example/";
|
||||||
|
const requestedUrls: string[] = [];
|
||||||
|
const fetchMock: typeof fetch = async (url) => {
|
||||||
|
requestedUrls.push(typeof url === "string" ? url : url.url);
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
authorization_endpoint: "https://auth.example.com/application/o/example/auth",
|
||||||
|
token_endpoint: "https://auth.example.com/application/o/example/token",
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await normalizeOidcProviderConfig(trailingIssuer, {}, fetchMock);
|
||||||
|
|
||||||
|
expect(requestedUrls[0]).toBe("https://auth.example.com/application/o/example/.well-known/openid-configuration");
|
||||||
|
expect(result.oidcConfig.discoveryEndpoint).toBe("https://auth.example.com/application/o/example/.well-known/openid-configuration");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws for invalid issuer URL", async () => {
|
||||||
|
await expect(
|
||||||
|
normalizeOidcProviderConfig("not-a-url", {}),
|
||||||
|
).rejects.toBeInstanceOf(OidcConfigError);
|
||||||
|
});
|
||||||
|
});
|
||||||
205
src/lib/sso/oidc-config.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const DEFAULT_SCOPES = ["openid", "email", "profile"] as const;
|
||||||
|
const DISCOVERY_TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
|
const discoverySchema = z.object({
|
||||||
|
issuer: z.string().url().optional(),
|
||||||
|
authorization_endpoint: z.string().url().optional(),
|
||||||
|
token_endpoint: z.string().url().optional(),
|
||||||
|
userinfo_endpoint: z.string().url().optional(),
|
||||||
|
jwks_uri: z.string().url().optional(),
|
||||||
|
scopes_supported: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export class OidcConfigError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "OidcConfigError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RawOidcConfig = {
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
authorizationEndpoint?: string;
|
||||||
|
tokenEndpoint?: string;
|
||||||
|
jwksEndpoint?: string;
|
||||||
|
userInfoEndpoint?: string;
|
||||||
|
discoveryEndpoint?: string;
|
||||||
|
scopes?: string[];
|
||||||
|
pkce?: boolean;
|
||||||
|
mapping?: ProviderMapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderMapping = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
emailVerified?: string;
|
||||||
|
name?: string;
|
||||||
|
image?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NormalizedOidcConfig = {
|
||||||
|
oidcConfig: {
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
authorizationEndpoint: string;
|
||||||
|
tokenEndpoint: string;
|
||||||
|
jwksEndpoint?: string;
|
||||||
|
userInfoEndpoint?: string;
|
||||||
|
discoveryEndpoint: string;
|
||||||
|
scopes: string[];
|
||||||
|
pkce: boolean;
|
||||||
|
};
|
||||||
|
mapping: ProviderMapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FetchFn = typeof fetch;
|
||||||
|
|
||||||
|
function cleanUrl(value: string | undefined, field: string): string | undefined {
|
||||||
|
if (!value || typeof value !== "string") return undefined;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
try {
|
||||||
|
return new URL(trimmed).toString();
|
||||||
|
} catch {
|
||||||
|
throw new OidcConfigError(`Invalid ${field} URL: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeScopes(scopes: string[] | undefined, fallback: readonly string[]): string[] {
|
||||||
|
const candidates = Array.isArray(scopes) ? scopes : [];
|
||||||
|
const sanitized = candidates
|
||||||
|
.map(scope => scope?.trim())
|
||||||
|
.filter((scope): scope is string => Boolean(scope));
|
||||||
|
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
return [...fallback];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Set(sanitized));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDiscoveryDocument(url: string, fetchFn: FetchFn): Promise<z.infer<typeof discoverySchema>> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchFn(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new OidcConfigError(`OIDC discovery request failed (${response.status} ${response.statusText})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: unknown;
|
||||||
|
try {
|
||||||
|
payload = await response.json();
|
||||||
|
} catch {
|
||||||
|
throw new OidcConfigError("OIDC discovery response is not valid JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = discoverySchema.parse(payload);
|
||||||
|
if (!parsed.authorization_endpoint || !parsed.token_endpoint) {
|
||||||
|
throw new OidcConfigError("OIDC discovery document is missing required endpoints");
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof OidcConfigError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
throw new OidcConfigError(`OIDC discovery timed out after ${DISCOVERY_TIMEOUT_MS / 1000}s`);
|
||||||
|
}
|
||||||
|
throw new OidcConfigError(`Failed to fetch OIDC discovery document: ${error instanceof Error ? error.message : "unknown error"}`);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function normalizeOidcProviderConfig(
|
||||||
|
issuer: string,
|
||||||
|
rawConfig: RawOidcConfig,
|
||||||
|
fetchFn: FetchFn = fetch,
|
||||||
|
): Promise<NormalizedOidcConfig> {
|
||||||
|
if (!issuer || typeof issuer !== "string") {
|
||||||
|
throw new OidcConfigError("Issuer is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedIssuer = issuer.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate issuer but keep caller-provided formatting so we don't break provider expectations
|
||||||
|
new URL(trimmedIssuer);
|
||||||
|
} catch {
|
||||||
|
throw new OidcConfigError(`Invalid issuer URL: ${issuer}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuerForDiscovery = trimmedIssuer.replace(/\/$/, "");
|
||||||
|
|
||||||
|
const discoveryEndpoint = cleanUrl(
|
||||||
|
rawConfig.discoveryEndpoint,
|
||||||
|
"discovery endpoint",
|
||||||
|
) ?? `${issuerForDiscovery}/.well-known/openid-configuration`;
|
||||||
|
|
||||||
|
const authorizationEndpoint = cleanUrl(rawConfig.authorizationEndpoint, "authorization endpoint");
|
||||||
|
const tokenEndpoint = cleanUrl(rawConfig.tokenEndpoint, "token endpoint");
|
||||||
|
const jwksEndpoint = cleanUrl(rawConfig.jwksEndpoint, "JWKS endpoint");
|
||||||
|
const userInfoEndpoint = cleanUrl(rawConfig.userInfoEndpoint, "userinfo endpoint");
|
||||||
|
const providedScopes = Array.isArray(rawConfig.scopes) ? rawConfig.scopes : undefined;
|
||||||
|
let scopes = sanitizeScopes(providedScopes, DEFAULT_SCOPES);
|
||||||
|
|
||||||
|
const shouldFetchDiscovery =
|
||||||
|
!authorizationEndpoint ||
|
||||||
|
!tokenEndpoint ||
|
||||||
|
!jwksEndpoint ||
|
||||||
|
!userInfoEndpoint ||
|
||||||
|
!providedScopes ||
|
||||||
|
providedScopes.length === 0;
|
||||||
|
|
||||||
|
let resolvedAuthorization = authorizationEndpoint;
|
||||||
|
let resolvedToken = tokenEndpoint;
|
||||||
|
let resolvedJwks = jwksEndpoint;
|
||||||
|
let resolvedUserInfo = userInfoEndpoint;
|
||||||
|
|
||||||
|
if (shouldFetchDiscovery) {
|
||||||
|
const discovery = await fetchDiscoveryDocument(discoveryEndpoint, fetchFn);
|
||||||
|
resolvedAuthorization = resolvedAuthorization ?? discovery.authorization_endpoint;
|
||||||
|
resolvedToken = resolvedToken ?? discovery.token_endpoint;
|
||||||
|
resolvedJwks = resolvedJwks ?? discovery.jwks_uri;
|
||||||
|
resolvedUserInfo = resolvedUserInfo ?? discovery.userinfo_endpoint;
|
||||||
|
if (!providedScopes || providedScopes.length === 0) {
|
||||||
|
scopes = sanitizeScopes(discovery.scopes_supported, DEFAULT_SCOPES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedAuthorization || !resolvedToken) {
|
||||||
|
throw new OidcConfigError("OIDC configuration must include authorization and token endpoints");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
oidcConfig: {
|
||||||
|
clientId: rawConfig.clientId,
|
||||||
|
clientSecret: rawConfig.clientSecret,
|
||||||
|
authorizationEndpoint: resolvedAuthorization,
|
||||||
|
tokenEndpoint: resolvedToken,
|
||||||
|
jwksEndpoint: resolvedJwks,
|
||||||
|
userInfoEndpoint: resolvedUserInfo,
|
||||||
|
discoveryEndpoint,
|
||||||
|
scopes,
|
||||||
|
pkce: rawConfig.pkce !== false,
|
||||||
|
},
|
||||||
|
mapping: rawConfig.mapping ?? {
|
||||||
|
id: "sub",
|
||||||
|
email: "email",
|
||||||
|
emailVerified: "email_verified",
|
||||||
|
name: "name",
|
||||||
|
image: "picture",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import { jsonResponse, formatDate, truncate, safeParse, parseErrorMessage, showErrorToast } from "./utils";
|
import { jsonResponse, formatDate, formatDateShort, truncate, safeParse, parseErrorMessage, showErrorToast } from "./utils";
|
||||||
|
|
||||||
describe("jsonResponse", () => {
|
describe("jsonResponse", () => {
|
||||||
test("creates a Response with JSON content", () => {
|
test("creates a Response with JSON content", () => {
|
||||||
@@ -65,6 +65,18 @@ describe("formatDate", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("formatDateShort", () => {
|
||||||
|
test("returns formatted date when input is provided", () => {
|
||||||
|
const formatted = formatDateShort("2014-10-20T15:32:10Z");
|
||||||
|
expect(formatted).toBe("Oct 20, 2014");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when date is missing", () => {
|
||||||
|
expect(formatDateShort(null)).toBeUndefined();
|
||||||
|
expect(formatDateShort(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("truncate", () => {
|
describe("truncate", () => {
|
||||||
test("truncates a string that exceeds the length", () => {
|
test("truncates a string that exceeds the length", () => {
|
||||||
const str = "This is a long string that needs truncation";
|
const str = "This is a long string that needs truncation";
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ export function formatDate(date?: Date | string | null): string {
|
|||||||
}).format(new Date(date));
|
}).format(new Date(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDateShort(date?: Date | string | null): string | undefined {
|
||||||
|
if (!date) return undefined;
|
||||||
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(new Date(date));
|
||||||
|
}
|
||||||
|
|
||||||
export function formatLastSyncTime(date: Date | string | null): string {
|
export function formatLastSyncTime(date: Date | string | null): string {
|
||||||
if (!date) return "Never";
|
if (!date) return "Never";
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export async function createDefaultConfig({ userId, envOverrides = {} }: Default
|
|||||||
addTopics: true,
|
addTopics: true,
|
||||||
preserveVisibility: false,
|
preserveVisibility: false,
|
||||||
forkStrategy: "reference",
|
forkStrategy: "reference",
|
||||||
|
issueConcurrency: 3,
|
||||||
|
pullRequestConcurrency: 5,
|
||||||
},
|
},
|
||||||
include: [],
|
include: [],
|
||||||
exclude: [],
|
exclude: [],
|
||||||
@@ -100,6 +102,13 @@ export async function createDefaultConfig({ userId, envOverrides = {} }: Default
|
|||||||
cleanupConfig: {
|
cleanupConfig: {
|
||||||
enabled: cleanupEnabled,
|
enabled: cleanupEnabled,
|
||||||
retentionDays: cleanupRetentionDays,
|
retentionDays: cleanupRetentionDays,
|
||||||
|
deleteFromGitea: false,
|
||||||
|
deleteIfNotInGitHub: true,
|
||||||
|
protectedRepos: [],
|
||||||
|
dryRun: false,
|
||||||
|
orphanedRepoAction: "archive",
|
||||||
|
batchSize: 10,
|
||||||
|
pauseBetweenDeletes: 2000,
|
||||||
lastRun: null,
|
lastRun: null,
|
||||||
nextRun: cleanupEnabled ? new Date(Date.now() + getCleanupInterval(cleanupRetentionDays) * 1000) : null,
|
nextRun: cleanupEnabled ? new Date(Date.now() + getCleanupInterval(cleanupRetentionDays) * 1000) : null,
|
||||||
},
|
},
|
||||||
@@ -123,4 +132,4 @@ function getCleanupInterval(retentionSeconds: number): number {
|
|||||||
if (days <= 7) return 86400; // 24 hours
|
if (days <= 7) return 86400; // 24 hours
|
||||||
if (days <= 30) return 172800; // 48 hours
|
if (days <= 30) return 172800; // 48 hours
|
||||||
return 604800; // 1 week
|
return 604800; // 1 week
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function mapUiToDbConfig(
|
|||||||
defaultOrg: giteaConfig.organization,
|
defaultOrg: giteaConfig.organization,
|
||||||
|
|
||||||
// Advanced options
|
// Advanced options
|
||||||
skipStarredIssues: advancedOptions.skipStarredIssues,
|
starredCodeOnly: advancedOptions.starredCodeOnly,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map Gitea config to match database schema
|
// Map Gitea config to match database schema
|
||||||
@@ -89,6 +89,8 @@ export function mapUiToDbConfig(
|
|||||||
forkStrategy: advancedOptions.skipForks ? "skip" : "reference",
|
forkStrategy: advancedOptions.skipForks ? "skip" : "reference",
|
||||||
|
|
||||||
// Mirror options from UI
|
// Mirror options from UI
|
||||||
|
issueConcurrency: giteaConfig.issueConcurrency ?? 3,
|
||||||
|
pullRequestConcurrency: giteaConfig.pullRequestConcurrency ?? 5,
|
||||||
mirrorReleases: mirrorOptions.mirrorReleases,
|
mirrorReleases: mirrorOptions.mirrorReleases,
|
||||||
releaseLimit: mirrorOptions.releaseLimit || 10,
|
releaseLimit: mirrorOptions.releaseLimit || 10,
|
||||||
mirrorMetadata: mirrorOptions.mirrorMetadata,
|
mirrorMetadata: mirrorOptions.mirrorMetadata,
|
||||||
@@ -132,6 +134,8 @@ export function mapDbToUiConfig(dbConfig: any): {
|
|||||||
preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
|
preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
|
||||||
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
|
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
|
||||||
personalReposOrg: undefined, // Not stored in current schema
|
personalReposOrg: undefined, // Not stored in current schema
|
||||||
|
issueConcurrency: dbConfig.giteaConfig?.issueConcurrency ?? 3,
|
||||||
|
pullRequestConcurrency: dbConfig.giteaConfig?.pullRequestConcurrency ?? 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map mirror options from various database fields
|
// Map mirror options from various database fields
|
||||||
@@ -152,7 +156,8 @@ export function mapDbToUiConfig(dbConfig: any): {
|
|||||||
// Map advanced options
|
// Map advanced options
|
||||||
const advancedOptions: AdvancedOptions = {
|
const advancedOptions: AdvancedOptions = {
|
||||||
skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
|
skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
|
||||||
skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false,
|
// Support both old (skipStarredIssues) and new (starredCodeOnly) field names for backward compatibility
|
||||||
|
starredCodeOnly: dbConfig.githubConfig?.starredCodeOnly ?? (dbConfig.githubConfig as any)?.skipStarredIssues ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -224,16 +229,26 @@ export function mapDbScheduleToUi(dbSchedule: DbScheduleConfig): any {
|
|||||||
* Maps UI cleanup config to database schema
|
* Maps UI cleanup config to database schema
|
||||||
*/
|
*/
|
||||||
export function mapUiCleanupToDb(uiCleanup: any): DbCleanupConfig {
|
export function mapUiCleanupToDb(uiCleanup: any): DbCleanupConfig {
|
||||||
|
const parsedRetention =
|
||||||
|
typeof uiCleanup.retentionDays === "string"
|
||||||
|
? parseInt(uiCleanup.retentionDays, 10)
|
||||||
|
: uiCleanup.retentionDays;
|
||||||
|
const retentionSeconds = Number.isFinite(parsedRetention)
|
||||||
|
? parsedRetention
|
||||||
|
: 604800;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: uiCleanup.enabled || false,
|
enabled: Boolean(uiCleanup.enabled),
|
||||||
retentionDays: uiCleanup.retentionDays || 604800, // Default to 7 days
|
retentionDays: retentionSeconds,
|
||||||
deleteFromGitea: false,
|
deleteFromGitea: uiCleanup.deleteFromGitea ?? false,
|
||||||
deleteIfNotInGitHub: true,
|
deleteIfNotInGitHub: uiCleanup.deleteIfNotInGitHub ?? true,
|
||||||
protectedRepos: [],
|
protectedRepos: uiCleanup.protectedRepos ?? [],
|
||||||
dryRun: true,
|
dryRun: uiCleanup.dryRun ?? false,
|
||||||
orphanedRepoAction: "archive",
|
orphanedRepoAction: (uiCleanup.orphanedRepoAction as DbCleanupConfig["orphanedRepoAction"]) || "archive",
|
||||||
batchSize: 10,
|
batchSize: uiCleanup.batchSize ?? 10,
|
||||||
pauseBetweenDeletes: 2000,
|
pauseBetweenDeletes: uiCleanup.pauseBetweenDeletes ?? 2000,
|
||||||
|
lastRun: uiCleanup.lastRun ?? null,
|
||||||
|
nextRun: uiCleanup.nextRun ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,9 +267,16 @@ export function mapDbCleanupToUi(dbCleanup: DbCleanupConfig): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: dbCleanup.enabled || false,
|
enabled: dbCleanup.enabled ?? false,
|
||||||
retentionDays: dbCleanup.retentionDays || 604800, // Use actual value from DB or default to 7 days
|
retentionDays: dbCleanup.retentionDays ?? 604800,
|
||||||
lastRun: dbCleanup.lastRun || null,
|
deleteFromGitea: dbCleanup.deleteFromGitea ?? false,
|
||||||
nextRun: dbCleanup.nextRun || null,
|
deleteIfNotInGitHub: dbCleanup.deleteIfNotInGitHub ?? true,
|
||||||
|
protectedRepos: dbCleanup.protectedRepos ?? [],
|
||||||
|
dryRun: dbCleanup.dryRun ?? false,
|
||||||
|
orphanedRepoAction: dbCleanup.orphanedRepoAction ?? "archive",
|
||||||
|
batchSize: dbCleanup.batchSize ?? 10,
|
||||||
|
pauseBetweenDeletes: dbCleanup.pauseBetweenDeletes ?? 2000,
|
||||||
|
lastRun: dbCleanup.lastRun ?? null,
|
||||||
|
nextRun: dbCleanup.nextRun ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||