Skip to content

fix(security): SECRET_KEY, CORS wildcard, SQL injection, command injection (#193 #195 #196 #197 #200) - #207

Merged
groupthinking merged 1 commit into
mainfrom
security/apply-conflicting-security-prs
May 27, 2026
Merged

fix(security): SECRET_KEY, CORS wildcard, SQL injection, command injection (#193 #195 #196 #197 #200)#207
groupthinking merged 1 commit into
mainfrom
security/apply-conflicting-security-prs

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

Applies five security fixes from PRs that had merge conflicts with #206. The conflicting hunks were all in tests/e2e/pipeline.test.ts which was already fixed better in #206; only the security-relevant changes are applied here.

Changes

File Fix Source PR
code_generator.py Replace hardcoded SECRET_KEY = \"your-secret-key-here\" with os.getenv(\"SECRET_KEY\", secrets.token_urlsafe(32)) #193
code_generator.py Restrict CORS allow_origins from [\"*\"] to explicit localhost origins #195
real_api_endpoints.py Read ALLOWED_ORIGINS from env var; default to localhost origins instead of [\"*\"] #196
database_cleanup_service.py Validate table name with ^[a-zA-Z0-9_]+$ regex before SQL use; quote with double quotes for PRAGMA/DELETE #197
deployment_manager.py Path traversal guard (resolve + is_dir check); add --ignore-scripts to npm install; use resolved_path for all cwd args #200
tests/unit/test_database_cleanup_security.py New unit tests for SQL injection prevention #197

Closes

Closes #193, #195, #196, #197, #200

https://claude.ai/code/session_01AgA9F82EwazbdB5R2f9nsd


Generated by Claude Code

…#196 #197 #200

Resolves conflicts manually — the conflicting hunks were all in tests/e2e/pipeline.test.ts
and .github/workflows/e2e-tests.yml which were already fixed better in #206.
The security-relevant changes are applied cleanly:

- code_generator.py (#193): replace hardcoded SECRET_KEY with os.getenv/secrets.token_urlsafe
- code_generator.py (#195): restrict CORS allow_origins from ["*"] to localhost origins
- real_api_endpoints.py (#196): read ALLOWED_ORIGINS from env; default to localhost origins
- database_cleanup_service.py (#197): validate table name with regex before SQL use;
  quote safe_table_name with double quotes for PRAGMA and DELETE statements
- deployment_manager.py (#200): add path traversal guard (resolve + is_dir check);
  add --ignore-scripts to npm install; use resolved_path for all cwd args
- tests/unit/test_database_cleanup_security.py: new unit tests for SQL injection prevention

Closes #193 #195 #196 #197 #200

https://claude.ai/code/session_01AgA9F82EwazbdB5R2f9nsd
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced authentication secret key generation with secure random fallback instead of hardcoded values
    • Restricted CORS policy to configured local origins only
    • Improved project path validation during deployment
    • Added SQL injection prevention for database cleanup operations
  • Tests

    • Introduced security test coverage for database cleanup functionality

Walkthrough

This PR hardens backend security across four dimensions: environment-driven secret management and CORS allowlisting in generated FastAPI apps, production API CORS restriction, safe deployment path resolution, and SQL injection prevention in database cleanup operations. All changes are backed by security tests.

Changes

Security Hardening for FastAPI Backend and Database Operations

Layer / File(s) Summary
Generated FastAPI app security
src/youtube_extension/backend/code_generator.py
Generated main.py now sources SECRET_KEY from environment with secrets.token_urlsafe(32) fallback, and replaces wildcard CORS (*) with a specific localhost allowlist (http://localhost:3000, http://localhost:8000, http://localhost:5173).
Production API CORS configuration
src/youtube_extension/backend/real_api_endpoints.py
CORS middleware parses ALLOWED_ORIGINS environment variable (comma-separated list) instead of allowing all origins.
Deployment validation and working directory safety
src/youtube_extension/backend/deployment_manager.py
verify_project resolves project path to canonical form, validates it is a directory (returns early on failure), and applies the resolved path consistently to all subprocesses (npm install --ignore-scripts, npm run build, npx tsc --noEmit).
Database cleanup SQL injection prevention
src/youtube_extension/backend/services/database_cleanup_service.py
Table name validation rejects names outside [a-zA-Z0-9_]+ pattern and constructs quoted safe_table_name for use in PRAGMA table_info and DELETE statements, preventing injection-style SQL.
Security test coverage for cleanup validation
tests/unit/test_database_cleanup_security.py
New test module with temp_db fixture validates: (1) cleanup succeeds on valid table names, (2) SQL injection attempts are rejected with error message, (3) tables remain unchanged after rejected cleanup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

security

Poem

🔐 Secrets born from the environment's breath,
CORS gates now guard against unwanted guests,
Paths resolve true, SQL bends to our will—
No wildcards, no injections, just safety fulfilled. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically identifies all five security fixes (SECRET_KEY, CORS wildcard, SQL injection, command injection) and references the source PRs, directly matching the changeset.
Description check ✅ Passed The description provides a detailed table mapping each file change to its corresponding security fix and source PR, clearly explaining the motivation and scope of the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/apply-conflicting-security-prs

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

🔴 E2E Test Results: FAILURE DETECTED

Metric Value
Status 🔴 RED
Total Tests
Passed
Failed 1
Deployment https://uvai.io
Test Output
vents fire in correct agent order
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mCloudEvent Schema�[2m > �[22mSSE events contain valid timestamps
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mCloudEvent Schema�[2m > �[22mterminal pipeline_status includes duration and agent count when present
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mError Handling�[2m > �[22mmissing URL returns 400, not a hang
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mError Handling�[2m > �[22minvalid URL returns error event or completes quickly, not a hang
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mDashboard�[2m > �[22m/dashboard returns 200
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mDashboard�[2m > �[22m/dashboard contains agent or pipeline visualization markup
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mAPI Health�[2m > �[22mGET /api returns a response (not 404)
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mAPI Health�[2m > �[22mPOST /api/pipeline/stream with no body returns 400
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mStatic Assets�[2m > �[22mhomepage has proper meta tags
 �[2m�[90m↓�[39m�[22m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment�[2m > �[22mStatic Assets�[2m > �[22m/features page returns 200

�[31m⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Suites 1 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m

�[41m�[1m FAIL �[22m�[49m tests/e2e/pipeline.test.ts�[2m > �[22mEventRelay E2E — Live Deployment
�[31m�[1mError�[22m: Hook timed out in 30000ms.
If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".�[39m
�[36m �[2m❯�[22m tests/e2e/pipeline.test.ts:�[2m87:3�[22m�[39m
    �[90m 85|�[39m �[34mdescribe�[39m(�[32m'EventRelay E2E — Live Deployment'�[39m�[33m,�[39m () �[33m=>�[39m {
    �[90m 86|�[39m   �[90m// Smoke check: is the site up?�[39m
    �[90m 87|�[39m   �[34mbeforeAll�[39m(�[35masync�[39m () �[33m=>�[39m {
    �[90m   |�[39m   �[31m^�[39m
    �[90m 88|�[39m     �[35mconst�[39m res �[33m=�[39m �[35mawait�[39m �[34mfetchWithTimeout�[39m(�[33mBASE_URL�[39m�[33m,�[39m {}�[33m,�[39m �[34m15_000�[39m)�[33m;�[39m
    �[90m 89|�[39m     �[35mif�[39m (�[33m!�[39mres�[33m.�[39mok) {

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m


�[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[90m (1)�[39m
�[2m      Tests �[22m �[33m17 skipped�[39m�[90m (17)�[39m
�[2m   Start at �[22m 12:36:13
�[2m   Duration �[22m 30.20s�[2m (transform 52ms, setup 0ms, import 66ms, tests 30.01s, environment 0ms)�[22m


@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several security enhancements, including dynamic SECRET_KEY generation, restricted CORS origins, path traversal validation during project verification, and SQL injection prevention in the database cleanup service. The feedback suggests ensuring that dynamically generated SECRET_KEYs do not disrupt multi-worker environments, verifying that resolved paths are constrained to allowed directories to fully mitigate path traversal, and stripping whitespace from the ALLOWED_ORIGINS environment variable to prevent CORS preflight failures.

auth_code = '''
# Authentication setup
SECRET_KEY = "your-secret-key-here"
SECRET_KEY = os.getenv("SECRET_KEY", secrets.token_urlsafe(32))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Generating a random SECRET_KEY on startup using secrets.token_urlsafe(32) is a great security improvement over a hardcoded key. However, in multi-worker production environments (e.g., running Uvicorn with --workers), each worker process will generate a different key. This will cause intermittent authentication failures as requests are load-balanced across workers.\n\nConsider adding a startup warning or documenting that SECRET_KEY must be explicitly set in the environment for multi-worker or multi-instance deployments.

Suggested change
SECRET_KEY = os.getenv("SECRET_KEY", secrets.token_urlsafe(32))
SECRET_KEY = os.getenv("SECRET_KEY")\nif not SECRET_KEY:\n import logging\n logging.warning("SECRET_KEY env var is missing! Generating a random key. This will break sessions/JWTs across restarts and multi-worker deployments.")\n SECRET_KEY = secrets.token_urlsafe(32)

Comment on lines +113 to +120
try:
resolved_path = project_dir.resolve()
if not resolved_path.is_dir():
result["summary"] = "Invalid project path: not a directory"
return result
except Exception as e:
result["summary"] = f"Invalid project path: {e}"
return result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

While calling resolve() resolves relative path segments (like ..), it does not prevent path traversal on its own unless you verify that the resolved path remains within an allowed base directory. An attacker could still pass a path like /etc or ../../../../etc which resolves to a valid directory outside the workspace, allowing arbitrary directory access or command execution (via npm install in arbitrary directories).\n\nTo prevent path traversal, verify that the resolved path is relative to either the project root (uvai_root) or the system temporary directory.

        try:\n            resolved_path = project_dir.resolve()\n            if not resolved_path.is_dir():\n                result["summary"] = "Invalid project path: not a directory"\n                return result\n            import tempfile\n            temp_path = Path(tempfile.gettempdir()).resolve()\n            if not (resolved_path.is_relative_to(uvai_root.resolve()) or resolved_path.is_relative_to(temp_path)):\n                result["summary"] = "Access denied: path is outside allowed directories"\n                return result\n        except Exception as e:\n            result["summary"] = f"Invalid project path: {e}"\n            return result

Comment on lines +457 to +460
allowed_origins = os.getenv(
"ALLOWED_ORIGINS",
"http://localhost:3000,http://localhost:5173,http://localhost:8080,http://localhost:3001"
).split(",")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When splitting ALLOWED_ORIGINS by comma, any leading or trailing whitespace in the environment variable (e.g., http://localhost:3000, http://localhost:5173) will result in invalid origin strings with spaces. This will cause CORS preflight requests to fail in the browser.\n\nConsider stripping whitespace from each origin after splitting.

    allowed_origins = [\n        origin.strip()\n        for origin in os.getenv(\n            "ALLOWED_ORIGINS",\n            "http://localhost:3000,http://localhost:5173,http://localhost:8080,http://localhost:3001"\n        ).split(",")\n        if origin.strip()\n    ]

Comment on lines +8 to +12
from youtube_extension.backend.services.database_cleanup_service import (
CleanupResult,
DatabaseCleanupService,
RetentionPolicy,
)
@groupthinking
groupthinking merged commit b03b1e5 into main May 27, 2026
17 of 19 checks passed
@groupthinking
groupthinking deleted the security/apply-conflicting-security-prs branch May 27, 2026 12:39
groupthinking added a commit that referenced this pull request May 27, 2026
* chore: trigger uvai.io production deploy

* feat: UVAI UI/UX full refactor — features page, pricing, Nav, LandingNav (#205)

Zero emoji, real product mockups per feature section, SVG icons everywhere, LandingNav with proper cross-page routing and active states.

Co-authored-by: v0[bot] <v0[bot]@users.noreply.github.com>

* test: improve test coverage, fix Vercel build, dashboard auto-select, E2E resilience (#206)

- 155 new unit tests (middleware, API models, error handling)
- Fix Vercel ERESOLVE build failure via .npmrc legacy-peer-deps
- Fix dashboard ?video= URL param auto-select (issue #159)
- Fix next.config.js duplicate redirects/headers
- Fix CI test dependency installation for Python 3.12
- Make SSE stream always emit terminal pipeline_status event
- Make E2E tests resilient to live server degraded mode
- Replace banned dQw4w9WgXcQ video ID with auJzb1D-fag throughout

* fix: remove hardcoded Grok API key (#194)

🎯 What: Removed the hardcoded fallback value for the GROK_API_KEY in TriModelConsensusTool.
⚠️ Risk: Hardcoded API keys in source code can be exploited if the codebase is exposed or leaked, leading to unauthorized API access, quota exhaustion, and potential financial loss.
🛡️ Solution: Removed the hardcoded string so the tool relies strictly on the environment variable, aligning with secure configuration management practices.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

* feat(web): add privacy/terms/api-docs/app/login routes; noindex prototype pages (#204)

Resolves 404s on /privacy, /terms, /api/docs, /app, /login and ensures
/prototype carries a noindex robots tag. Scope is intentionally narrow and
does not overlap PR #202 (assets, robots, sitemap, JSON-LD, a11y).

- /privacy, /terms: server-rendered legal placeholder pages with proper
  metadata, canonical URLs, and footer links. Plain-language, startup-
  friendly; will be replaced before enterprise contracts.
- /api/docs: human-readable reference matching the documentation pointer
  returned by /api JSON. Lists actual /api/* routes that exist in code.
- /app, /login: server redirects to /dashboard, marked noindex. UVAI has
  no auth gate today, so this matches actual product behavior.
- /prototype: adds a route layout with robots.index=false because the
  underlying page is an internal prototype spec, not a public surface.
- LandingFooter: surfaces Privacy and Terms links now that the pages
  exist.

Build: next build succeeds, 25 routes generated. Type-check and ESLint clean.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* test: add unit tests for DatabaseOptimizer._calculate_performance_grade (#182)

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

* fix(security): SECRET_KEY, CORS wildcard, SQL injection, command injection (#207)

Resolves conflicts manually — the conflicting hunks were all in tests/e2e/pipeline.test.ts
and .github/workflows/e2e-tests.yml which were already fixed better in #206.
The security-relevant changes are applied cleanly:

- code_generator.py (#193): replace hardcoded SECRET_KEY with os.getenv/secrets.token_urlsafe
- code_generator.py (#195): restrict CORS allow_origins from ["*"] to localhost origins
- real_api_endpoints.py (#196): read ALLOWED_ORIGINS from env; default to localhost origins
- database_cleanup_service.py (#197): validate table name with regex before SQL use;
  quote safe_table_name with double quotes for PRAGMA and DELETE statements
- deployment_manager.py (#200): add path traversal guard (resolve + is_dir check);
  add --ignore-scripts to npm install; use resolved_path for all cwd args
- tests/unit/test_database_cleanup_security.py: new unit tests for SQL injection prevention

Closes #193 #195 #196 #197 #200

https://claude.ai/code/session_01AgA9F82EwazbdB5R2f9nsd

Co-authored-by: Claude <noreply@anthropic.com>

* chore: move legacy .agent content under .github (#162)

* chore: move legacy agent files into .github

Agent-Logs-Url: https://github.com/groupthinking/EventRelay/sessions/8e79c6e5-9755-40a2-b8b0-73b9ff75249e

Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com>

* docs: fix relocated agent references

Agent-Logs-Url: https://github.com/groupthinking/EventRelay/sessions/8e79c6e5-9755-40a2-b8b0-73b9ff75249e

Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com>

* docs: remove vague agent rule reference

Agent-Logs-Url: https://github.com/groupthinking/EventRelay/sessions/8e79c6e5-9755-40a2-b8b0-73b9ff75249e

Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com>

* chore(web): UVAI Phase 1 — SEO, a11y, missing static assets

Live uvai.io referenced /favicon.ico, /icon.svg, /apple-touch-icon.png but
apps/web/public/ did not exist in the repo, producing 404s. Layout metadata
also pointed metadataBase and og.url at the legacy v0-uvai.vercel.app host
rather than the canonical uvai.io domain.

Changes:
- Add apps/web/public with favicon.ico (multi-res), icon.svg, apple-touch-icon.png,
  og-image.png (1200x630), manifest.json, robots.txt.
- Add apps/web/src/app/sitemap.ts (Next.js Metadata Route sitemap).
- layout.tsx: metadataBase + og.url -> https://uvai.io, add alternates.canonical,
  inject Organization/WebSite/SoftwareApplication JSON-LD, add skip-to-main link.
- page.tsx: <main id=\"main\"> as skip-link target.
- LandingNav.tsx: aria-label=\"Primary\" on nav, aria-labels on brand + GitHub
  external link, visible focus rings on all interactive elements.
- HeroSection.tsx: focus rings on CTAs, honor prefers-reduced-motion for marquee.
- Add CHANGELOG.md with timestamped entry.

Local verification:
- npx eslint on touched files: clean
- npm run build (apps/web): success, /sitemap.xml route generated, TypeScript clean

No production / deploy / DNS / secret changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(web): PR #202 review fixes — no dangerouslySetInnerHTML, MD022, skip-link in layout, central SITE_URL

Addresses code review feedback on PR #202.

1. layout.tsx: replace dangerouslySetInnerHTML JSON-LD with React
   <script>{jsonLdString}</script> children. jsonLdString escapes
   `<` -> `<` to prevent any nested `</script>` breakout. Build
   verified: rendered HTML contains exactly one valid JSON-LD block.
   Complies with repo policy that forbids dangerouslySetInnerHTML.

2. CHANGELOG.md: markdownlint MD022 — blank lines after `#### Added`,
   `### Changed`, and `### Notes / known follow-ups (not in this change)`.

3. Skip-to-main-content target moved from `apps/web/src/app/page.tsx`'s
   <main> to the root layout's content wrapper. The link now works on
   every route (dashboard, pricing, features, playground, prototype,
   not-found), not just the homepage. Duplicate `id="main"` removed
   from page.tsx — rendered HTML on `/` now contains exactly one
   `id="main"`.

4. Add apps/web/src/lib/site.ts exporting SITE_URL = 'https://uvai.io'.
   layout.tsx (metadataBase, alternates.canonical, og.url, JSON-LD URLs)
   and sitemap.ts both consume it. Pricing/playground references kept
   as-is — those are mailto: addresses and api.uvai.io examples in
   code samples, not the same axis as the site origin.

Failing CI check (E2E Pipeline Tests) is unrelated to this PR:
- E2E runs vitest against BASE_URL=https://uvai.io (the live deployment)
- Live root returns 200 but is stale (title still "UVAI — Video to Software")
- This PR touches zero files under tests/e2e/, src/youtube_extension/,
  or apps/web/src/app/api/
- Resolution requires a redeploy of the current main, which is outside
  this PR's scope per the original instructions

Local verification:
- npx eslint on touched files: clean (exit 0)
- npm run build (apps/web): ✓ Compiled, TypeScript clean, 21 pages
- Rendered HTML inspection: JSON-LD block present and well-formed;
  id="main" present on /, /pricing, /features, /dashboard, /playground;
  exactly one id="main" on each prerendered page (no duplicates)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: v0[bot] <v0[bot]@users.noreply.github.com>
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Code <claude-code@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants