stkit 17.2.5__tar.gz → 17.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stkit-17.2.5 → stkit-17.2.7}/CHANGELOG.md +15 -0
- {stkit-17.2.5 → stkit-17.2.7}/PKG-INFO +2 -2
- {stkit-17.2.5 → stkit-17.2.7}/pyproject.toml +2 -2
- {stkit-17.2.5 → stkit-17.2.7}/stk/__init__.py +1 -1
- {stkit-17.2.5 → stkit-17.2.7}/stk/app.py +3 -2
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/copy_rules.py +5 -2
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/doctor.py +13 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/files/__init__.py +13 -1
- {stkit-17.2.5 → stkit-17.2.7}/stk/security.py +25 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_cli.py +10 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_copy_rules.py +3 -1
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_files.py +13 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_security_compatibility.py +29 -0
- {stkit-17.2.5 → stkit-17.2.7}/.gitignore +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/LICENSE +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/README.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/SECURITY.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/.env-sample +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/.gitignore +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/README.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/app.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/migrations/364232ae77d5_add_note.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/note/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/note/models.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/note/templates/note/index.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/note/views.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/settings.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/minimal/templates/layout.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/pyproject.toml +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/tests/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/tests/test_app.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/examples/minimal/uv.lock +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/__main__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/agent_login.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/checks.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/agent.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/base.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/database.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/develop.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/main.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/reports.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/smoke.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/users.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/cli/watch.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/extensions.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/files/models.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/files/store.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/files/views.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/mcp/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/mcp/models.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/mcp/server.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/mcp/views.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/env.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/script.py.mako +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/versions/stk_0001_initial_schema.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/versions/stk_0002_jobs.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/versions/stk_0003_files.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/versions/stk_0004_notifications.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/versions/stk_0005_mcp_tokens.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/migrations/versions/stk_0006_stk_mcp_token.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/notification/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/notification/models.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/notification/views.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/portal/views.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/public/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/public/models.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/public/views.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/CLAUDE.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/Dockerfile +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/README.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/__app__/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/__app__/app.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/__app__/settings.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/claude/skills/stk-api/SKILL.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/claude/skills/stk-blueprint/SKILL.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/claude/skills/stk-deploy/SKILL.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/claude/skills/stk-lessons/SKILL.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/claude/skills/stk-migrate/SKILL.md +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/docker-compose.yml +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/dockerignore +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/env-sample +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/github/workflows/checks.yml +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/gitignore +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/nginx/nginx.conf +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/nginx/stk.conf +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/postgres/init-db.sh +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/pyproject.toml +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/tests/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/app/tests/test_app.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/generator.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/scaffold/templates.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/settings.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/shell.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/VERSIONS.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/css/app.css +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/css/layout.css +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/css/vuetify.min.css +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.animals.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.arrows-1.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.arrows-2.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.badges.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.brand-1.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.brand-2.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.buildings.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.charts.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.communication.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.computers.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.core.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.currencies.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.database.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.design-1.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.design-2.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.development.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.devices-1.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.devices-2.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.document.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.e-commerce.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.electrical.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.extensions.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.food.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.games.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.gender.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.gestures.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.health.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.laundry.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.letters.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.logic.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.map.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.math.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.media.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.mood.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.nature.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.numbers.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.photography.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.shapes.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.sport.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.symbols.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.system-1.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.system-2.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.system-3.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.text.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.vehicles.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.version-control.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.weather.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/fonts/tabler-icons.zodiac.woff2 +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/icons/tabler-icons.min.css +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/img/auth-bg.webp +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/img/favicon.ico +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/img/stk.svg +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/axios.min.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/components/NotificationDropdown.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/components/ThemeSwitcher.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/components/TransitionExpand.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/components/VerticalNavGroup.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/components/VerticalNavLink.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/components/VerticalNavSectionTitle.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/components/index.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/config.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/vue.min.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/js/vuetify.min.js +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/static/robots.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/tasks/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/tasks/models.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/401.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/404.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/500.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/_brand.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/auth_layout.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/cms/activities.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/cms/jobs.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/cms/roles.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/cms/users.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/dashboard.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/index.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/layout.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/login-layout.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/mcp/tokens.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/_macros.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/_messages.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/change_password.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/confirm_email.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/change_notice.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/change_notice.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/confirmation_instructions.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/confirmation_instructions.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/login_instructions.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/login_instructions.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/reset_instructions.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/reset_instructions.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/reset_notice.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/reset_notice.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/welcome.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/email/welcome.txt +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/forgot_password.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/login_user.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/mf_recovery.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/mf_recovery_codes.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/register_user.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/reset_password.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/send_confirmation.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/send_login.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/two_factor_select.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/two_factor_setup.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/two_factor_verify_code.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/wan_register.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/wan_signin.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/templates/security/wan_verify.html +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/user/__init__.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/user/forms.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/user/models.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/user/views.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/utils/email.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/utils/netguard.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/utils/ratelimit.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/stk/websocket.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_agent_operability.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_app_index.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_branding.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_builder_ux.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_composition.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_csrf_exempt.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_error_handler.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_jobs.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_mcp.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_mcp_migration.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_migration_drift.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_netguard.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_notifications.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_production_security.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_revision_imports.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_route_collisions.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_scaffolder.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_shell.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_smoke_pages.py +0 -0
- {stkit-17.2.5 → stkit-17.2.7}/tests/test_static_versioning.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v17.2.7 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
No migration required. Requires quart-security 2.0.6.
|
|
6
|
+
|
|
7
|
+
- Registration no longer reveals which emails have an account: an existing email gets the same answer as a new one and nothing is created. With `MAIL_SERVER` set, the owner is emailed in the background that someone tried; its link comes from `STK_PUBLIC_URL`, never the request's Host header (Authentication).
|
|
8
|
+
- After login, users land back on the page that sent them to `/login`, including through 2FA and passkeys (Authentication).
|
|
9
|
+
- `stk.files` refuses to start when `STK_UPLOAD_MAX_BYTES` does not fit under Quart's `MAX_CONTENT_LENGTH`. Before, any limit above about 16 MiB was silently capped with a generic 413 (Configuration).
|
|
10
|
+
- `stk doctor` warns when `STK_PUBLIC_URL` is unset outside development, since any Host header is then accepted.
|
|
11
|
+
|
|
12
|
+
## v17.2.6 (2026-10-31)
|
|
13
|
+
|
|
14
|
+
No migration required.
|
|
15
|
+
|
|
16
|
+
- `stk smoke`'s `hex-id` rule no longer flags mixed-case names such as `Ed25519`. Ids are printed in one case, so `1fbaac41` and `1FBAAC41` still fail (Build Loop).
|
|
17
|
+
|
|
3
18
|
## v17.2.5 (2026-10-31)
|
|
4
19
|
|
|
5
20
|
No migration required.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: stkit
|
|
3
|
-
Version: 17.2.
|
|
3
|
+
Version: 17.2.7
|
|
4
4
|
Summary: An async Quart framework optimized for AI-assisted development workflows
|
|
5
5
|
Project-URL: Documentation, https://docs.stkit.dev
|
|
6
6
|
Project-URL: Source, https://github.com/level09/stk
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pillow>=13.0.0
|
|
|
28
28
|
Requires-Dist: psycopg2-binary>=2.9.10
|
|
29
29
|
Requires-Dist: python-dotenv>=1.2.2
|
|
30
30
|
Requires-Dist: qrcode>=8.2
|
|
31
|
-
Requires-Dist: quart-security<3,>=2.0.
|
|
31
|
+
Requires-Dist: quart-security<3,>=2.0.6
|
|
32
32
|
Requires-Dist: quart-session>=3.0.0
|
|
33
33
|
Requires-Dist: quart>=0.20.0
|
|
34
34
|
Requires-Dist: rich>=14.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stkit"
|
|
3
|
-
version = "17.2.
|
|
3
|
+
version = "17.2.7"
|
|
4
4
|
description = "An async Quart framework optimized for AI-assisted development workflows"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Level09", email = "hello@level09.com"}
|
|
@@ -30,7 +30,7 @@ dependencies = [
|
|
|
30
30
|
"python-dotenv>=1.2.2",
|
|
31
31
|
"qrcode>=8.2",
|
|
32
32
|
"quart>=0.20.0",
|
|
33
|
-
"quart-security>=2.0.
|
|
33
|
+
"quart-security>=2.0.6,<3",
|
|
34
34
|
"quart-session>=3.0.0",
|
|
35
35
|
"rich>=14.0.0",
|
|
36
36
|
"sqlalchemy[asyncio]>=2.0.41",
|
|
@@ -18,7 +18,7 @@ from quart import (
|
|
|
18
18
|
send_from_directory,
|
|
19
19
|
url_for,
|
|
20
20
|
)
|
|
21
|
-
from quart_security import Security, SQLAlchemyUserDatastore
|
|
21
|
+
from quart_security import Security, SQLAlchemyUserDatastore, user_register_conflict
|
|
22
22
|
from quart_security.views import _ensure_csrf_token
|
|
23
23
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
24
24
|
from werkzeug.exceptions import HTTPException, NotFound
|
|
@@ -172,7 +172,7 @@ def register_extensions(app):
|
|
|
172
172
|
except TimeoutError:
|
|
173
173
|
app.logger.warning("Timed out while disposing SQLAlchemy engine")
|
|
174
174
|
|
|
175
|
-
from stk.security import register_security_controls
|
|
175
|
+
from stk.security import notify_register_conflict, register_security_controls
|
|
176
176
|
from stk.utils.ratelimit import register_rate_limits
|
|
177
177
|
|
|
178
178
|
register_rate_limits(app)
|
|
@@ -184,6 +184,7 @@ def register_extensions(app):
|
|
|
184
184
|
register_form=ExtendedRegisterForm,
|
|
185
185
|
change_password_form=OAuthAwareChangePasswordForm,
|
|
186
186
|
)
|
|
187
|
+
user_register_conflict.connect(notify_register_conflict, sender=app, weak=False)
|
|
187
188
|
# The library resets its session before loading the user. Share that session
|
|
188
189
|
# so signals and application routes participate in the same transaction.
|
|
189
190
|
app.before_request(_open_session)
|
|
@@ -83,14 +83,17 @@ def uuid(text):
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def hex_id(text):
|
|
86
|
-
"""6+ hex characters with at least two digits and a letter, alone
|
|
87
|
-
word: 'Request 1fbaac41'. Uuids are their own rule.
|
|
86
|
+
"""6+ hex characters in one case with at least two digits and a letter, alone
|
|
87
|
+
or glued to a word: 'Request 1fbaac41'. Uuids are their own rule. Mixed case
|
|
88
|
+
is a name, not an id: 'Ed25519'."""
|
|
88
89
|
text = UUID.sub(" ", text)
|
|
89
90
|
out = []
|
|
90
91
|
for m in HEX.finditer(text):
|
|
91
92
|
token = m.group()
|
|
92
93
|
if sum(c.isdigit() for c in token) < 2 or token.isdigit():
|
|
93
94
|
continue
|
|
95
|
+
if token not in (token.lower(), token.upper()):
|
|
96
|
+
continue
|
|
94
97
|
if HASH_LABEL.search(text[max(0, m.start() - 20) : m.start()]):
|
|
95
98
|
continue
|
|
96
99
|
out.append(_around(text, m))
|
|
@@ -193,6 +193,18 @@ def _check_agent_login(config):
|
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
def _check_public_url(config):
|
|
197
|
+
env = config.get("STK_ENV", "production")
|
|
198
|
+
if env != "development" and not config.get("STK_PUBLIC_URL"):
|
|
199
|
+
return _finding(
|
|
200
|
+
"public url",
|
|
201
|
+
WARN,
|
|
202
|
+
f"STK_PUBLIC_URL unset with STK_ENV={env}: any Host header is accepted",
|
|
203
|
+
"set STK_PUBLIC_URL to the origin users visit, e.g. https://app.example.com",
|
|
204
|
+
)
|
|
205
|
+
return _finding("public url", OK, config.get("STK_PUBLIC_URL") or "development")
|
|
206
|
+
|
|
207
|
+
|
|
196
208
|
def _check_browser():
|
|
197
209
|
try:
|
|
198
210
|
from playwright.sync_api import sync_playwright
|
|
@@ -244,6 +256,7 @@ def build_doctor_report():
|
|
|
244
256
|
_check_migrations(app),
|
|
245
257
|
_check_admin(),
|
|
246
258
|
_check_agent_login(app.config),
|
|
259
|
+
_check_public_url(app.config),
|
|
247
260
|
]
|
|
248
261
|
findings += [_check_assets(), _check_browser(), _check_repl()]
|
|
249
262
|
if any(item["status"] == FAIL for item in findings):
|
|
@@ -8,10 +8,22 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def init_app(app):
|
|
11
|
-
from stk.files.views import SNIFFED, bp, can_read
|
|
11
|
+
from stk.files.views import MULTIPART_SLACK, SNIFFED, bp, can_read
|
|
12
12
|
|
|
13
13
|
app.config.setdefault("STK_UPLOAD_DIR", str(Path(app.instance_path) / "files"))
|
|
14
14
|
app.config.setdefault("STK_UPLOAD_MAX_BYTES", 10 * 1024 * 1024)
|
|
15
|
+
# Quart caps every request body at MAX_CONTENT_LENGTH before a view runs, so
|
|
16
|
+
# a larger upload limit would never take effect.
|
|
17
|
+
limit, body_max = (
|
|
18
|
+
app.config["STK_UPLOAD_MAX_BYTES"],
|
|
19
|
+
app.config["MAX_CONTENT_LENGTH"],
|
|
20
|
+
)
|
|
21
|
+
if body_max is not None and limit + MULTIPART_SLACK > body_max:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"STK_UPLOAD_MAX_BYTES ({limit}) plus {MULTIPART_SLACK} bytes of"
|
|
24
|
+
f" multipart framing exceeds MAX_CONTENT_LENGTH ({body_max}): raise"
|
|
25
|
+
" MAX_CONTENT_LENGTH or lower STK_UPLOAD_MAX_BYTES"
|
|
26
|
+
)
|
|
15
27
|
app.config.setdefault(
|
|
16
28
|
"STK_UPLOAD_TYPES",
|
|
17
29
|
("image/png", "image/jpeg", "image/gif", "image/webp", "application/pdf"),
|
|
@@ -124,6 +124,31 @@ def register_security_controls(app):
|
|
|
124
124
|
return response
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
async def notify_register_conflict(app, user):
|
|
128
|
+
"""Tell an account's owner that someone tried to register with their email.
|
|
129
|
+
|
|
130
|
+
Registration answers as if it succeeded, so this mail is the owner's only
|
|
131
|
+
sign. It goes out in the background: waiting on SMTP would make this one
|
|
132
|
+
response slower and reveal the account. Links come from STK_PUBLIC_URL,
|
|
133
|
+
never the request's Host header, which the sender controls."""
|
|
134
|
+
if not app.config.get("MAIL_SERVER"):
|
|
135
|
+
app.logger.warning("Sign-up attempt for an existing account: no MAIL_SERVER")
|
|
136
|
+
return
|
|
137
|
+
from stk.utils.email import send_email
|
|
138
|
+
|
|
139
|
+
public_url = app.config.get("STK_PUBLIC_URL")
|
|
140
|
+
where = f" at {public_url}" if public_url else ""
|
|
141
|
+
sign_in = f"\n\nSign in at {public_url.rstrip('/')}/login" if public_url else ""
|
|
142
|
+
body = (
|
|
143
|
+
f"Someone tried to create a {app.config['APP_NAME']} account{where} with"
|
|
144
|
+
" this email address. You already have one, so nothing changed."
|
|
145
|
+
f"{sign_in}\n\nIf it was not you, you can ignore this email."
|
|
146
|
+
)
|
|
147
|
+
app.add_background_task(
|
|
148
|
+
send_email, "Sign-up attempt with your email", user.email, body
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
127
152
|
async def check_password_policy(password):
|
|
128
153
|
from quart import current_app
|
|
129
154
|
|
|
@@ -55,6 +55,16 @@ if __name__ == "__main__":
|
|
|
55
55
|
unittest.main()
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
class PublicUrlDoctorTest(unittest.TestCase):
|
|
59
|
+
def test_warns_only_outside_development_without_a_public_url(self):
|
|
60
|
+
from stk.cli.doctor import _check_public_url
|
|
61
|
+
|
|
62
|
+
self.assertEqual(_check_public_url({})["status"], "warn")
|
|
63
|
+
self.assertEqual(_check_public_url({"STK_ENV": "development"})["status"], "ok")
|
|
64
|
+
set_url = {"STK_PUBLIC_URL": "https://app.example.com"}
|
|
65
|
+
self.assertEqual(_check_public_url(set_url)["status"], "ok")
|
|
66
|
+
|
|
67
|
+
|
|
58
68
|
class DowngradeArgumentTest(unittest.TestCase):
|
|
59
69
|
def test_relative_revision_is_accepted(self):
|
|
60
70
|
with patch("stk.cli.database.run_alembic") as run:
|
|
@@ -31,9 +31,11 @@ class CopyRuleTests(unittest.TestCase):
|
|
|
31
31
|
def test_hex_id(self):
|
|
32
32
|
self.assertRule(
|
|
33
33
|
hex_id,
|
|
34
|
-
["Request 1fbaac41 failed", "texteb148f8f"],
|
|
34
|
+
["Request 1fbaac41 failed", "texteb148f8f", "Build 1FBAAC41"],
|
|
35
35
|
[
|
|
36
36
|
"SHA-256: 9f86d081884c7d65",
|
|
37
|
+
"Every run gets a fresh Ed25519 key",
|
|
38
|
+
"a facade from a decade ago",
|
|
37
39
|
"Total 1234567",
|
|
38
40
|
"Accepted",
|
|
39
41
|
"Owner 3f2b8c1e-9a4d-4e2f-8b1c-0d9e7f6a5b4c",
|
|
@@ -82,6 +82,19 @@ class FilesTests(unittest.IsolatedAsyncioTestCase):
|
|
|
82
82
|
def stored(self):
|
|
83
83
|
return sorted(p.name for p in Path(self.dir.name).rglob("*") if p.is_file())
|
|
84
84
|
|
|
85
|
+
def test_upload_limit_above_the_body_limit_refuses_to_start(self):
|
|
86
|
+
class Config(TestingConfig):
|
|
87
|
+
STK_UPLOAD_DIR = self.dir.name
|
|
88
|
+
STK_UPLOAD_MAX_BYTES = 20 * 1024 * 1024
|
|
89
|
+
|
|
90
|
+
with self.assertRaisesRegex(ValueError, "MAX_CONTENT_LENGTH"):
|
|
91
|
+
Stk(__name__, config=Config, extensions=[stk.files])
|
|
92
|
+
|
|
93
|
+
class Raised(Config):
|
|
94
|
+
MAX_CONTENT_LENGTH = 21 * 1024 * 1024
|
|
95
|
+
|
|
96
|
+
Stk(__name__, config=Raised, extensions=[stk.files])
|
|
97
|
+
|
|
85
98
|
async def test_same_bytes_are_stored_once(self):
|
|
86
99
|
alice, bob = await self.client(), await self.client("bob@example.com")
|
|
87
100
|
data = png()
|
|
@@ -146,5 +146,34 @@ class SecurityCompatibilityTests(unittest.IsolatedAsyncioTestCase):
|
|
|
146
146
|
)
|
|
147
147
|
self.assertNotEqual((await second.get("/dashboard/")).status_code, 200)
|
|
148
148
|
|
|
149
|
+
async def test_registering_an_existing_email_mails_the_owner_in_background(self):
|
|
150
|
+
self.app.config.update(
|
|
151
|
+
SECURITY_REGISTERABLE=True,
|
|
152
|
+
MAIL_SERVER="smtp.example.com",
|
|
153
|
+
STK_PUBLIC_URL="http://localhost",
|
|
154
|
+
)
|
|
155
|
+
queued = []
|
|
156
|
+
self.app.add_background_task = lambda func, *args: queued.append(args)
|
|
157
|
+
client = self.app.test_client()
|
|
158
|
+
await client.get("/login")
|
|
159
|
+
async with client.session_transaction() as cookie:
|
|
160
|
+
csrf = cookie["_csrf_token"]
|
|
161
|
+
form = {
|
|
162
|
+
"email": "USER@example.com",
|
|
163
|
+
"password": "AnotherPassword123!",
|
|
164
|
+
"password_confirm": "AnotherPassword123!",
|
|
165
|
+
"csrf_token": csrf,
|
|
166
|
+
}
|
|
167
|
+
response = await client.post("/register", form=form)
|
|
168
|
+
self.assertEqual(response.status_code, 302)
|
|
169
|
+
self.assertEqual(len(queued), 1)
|
|
170
|
+
subject, recipient, body = queued[0]
|
|
171
|
+
self.assertEqual(recipient, "user@example.com")
|
|
172
|
+
self.assertIn("http://localhost/login", body)
|
|
173
|
+
# A forged Host header never reaches the mail's links.
|
|
174
|
+
self.app.config["STK_PUBLIC_URL"] = None
|
|
175
|
+
await client.post("/register", form=form, headers={"Host": "evil.example"})
|
|
176
|
+
self.assertNotIn("evil.example", queued[1][2])
|
|
177
|
+
|
|
149
178
|
def test_cookie_setting_preserves_local_http_support(self):
|
|
150
179
|
self.assertFalse(self.app.config["SESSION_COOKIE_SECURE"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|