create-workframe 0.1.0
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.
- package/.dockerignore +22 -0
- package/.gitignore +73 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +111 -0
- package/SECURITY.md +40 -0
- package/bin/create-workframe.js +2814 -0
- package/bin/workframe.js +329 -0
- package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -0
- package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -0
- package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -0
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -0
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -0
- package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -0
- package/package.json +67 -0
- package/profiles/README.md +15 -0
- package/profiles/architect/AGENTS.md +29 -0
- package/profiles/architect/SOUL.md +44 -0
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/designer/AGENTS.md +26 -0
- package/profiles/designer/SOUL.md +31 -0
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/dev/AGENTS.md +28 -0
- package/profiles/dev/SOUL.md +31 -0
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/docs/AGENTS.md +27 -0
- package/profiles/docs/SOUL.md +30 -0
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/research/AGENTS.md +26 -0
- package/profiles/research/SOUL.md +31 -0
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/visionary/AGENTS.md +25 -0
- package/profiles/visionary/SOUL.md +31 -0
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/workframe-agent/AGENTS.md +37 -0
- package/profiles/workframe-agent/SETUP.md +185 -0
- package/profiles/workframe-agent/SOUL.md +61 -0
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -0
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -0
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -0
- package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -0
- package/rules/.hermes.md +11 -0
- package/rules/AGENTS.md +22 -0
- package/rules/workspace-README.md +5 -0
- package/scripts/apply-update-hermes.sh +17 -0
- package/scripts/apply-update-workframe.sh +77 -0
- package/scripts/bootstrap-workspace-link.sh +8 -0
- package/scripts/bundle-workframe-ui.mjs +77 -0
- package/scripts/compose-docker-host.sh +37 -0
- package/scripts/create_workframe_scaffold.py +648 -0
- package/scripts/ensure-compose-host-paths.mjs +51 -0
- package/scripts/fix-zk-encryption-key.sh +35 -0
- package/scripts/lib/install-identity.mjs +212 -0
- package/scripts/lib/workframe-registry.mjs +290 -0
- package/scripts/new-project.mjs +68 -0
- package/scripts/restart-gateway-hermes.sh +12 -0
- package/scripts/security_audit.py +156 -0
- package/scripts/select_agent_pack.py +31 -0
- package/scripts/set-compose-public-url.mjs +92 -0
- package/scripts/setup-stack-secrets.sh +50 -0
- package/scripts/sync-canonical-to-package.mjs +146 -0
- package/scripts/test-scaffold.mjs +390 -0
- package/scripts/verify-public-deploy.sh +105 -0
- package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -0
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -0
- package/shared/WORKFRAME_AGENT_PACKS.json +64 -0
- package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -0
- package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -0
- package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -0
- package/shared/WORKFRAME_SKILL_CURATION.md +27 -0
- package/shared/agent-avatars/ada.png +0 -0
- package/shared/agent-avatars/aibert.png +0 -0
- package/shared/agent-avatars/amelia.png +0 -0
- package/shared/agent-avatars/andy.png +0 -0
- package/shared/agent-avatars/arc.png +0 -0
- package/shared/agent-avatars/bob.png +0 -0
- package/shared/agent-avatars/buzz.png +0 -0
- package/shared/agent-avatars/carl.png +0 -0
- package/shared/agent-avatars/catalog.json +171 -0
- package/shared/agent-avatars/corbu.png +0 -0
- package/shared/agent-avatars/diana.png +0 -0
- package/shared/agent-avatars/ella.png +0 -0
- package/shared/agent-avatars/elvis.png +0 -0
- package/shared/agent-avatars/f1.png +0 -0
- package/shared/agent-avatars/f2.png +0 -0
- package/shared/agent-avatars/f3.png +0 -0
- package/shared/agent-avatars/f4.png +0 -0
- package/shared/agent-avatars/f5.png +0 -0
- package/shared/agent-avatars/f6.png +0 -0
- package/shared/agent-avatars/frida.png +0 -0
- package/shared/agent-avatars/george.png +0 -0
- package/shared/agent-avatars/grace.png +0 -0
- package/shared/agent-avatars/hedy.png +0 -0
- package/shared/agent-avatars/hermes.png +0 -0
- package/shared/agent-avatars/isaac.png +0 -0
- package/shared/agent-avatars/jes.png +0 -0
- package/shared/agent-avatars/john.png +0 -0
- package/shared/agent-avatars/joni.png +0 -0
- package/shared/agent-avatars/leo.png +0 -0
- package/shared/agent-avatars/louis.png +0 -0
- package/shared/agent-avatars/ludwig.png +0 -0
- package/shared/agent-avatars/m1.png +0 -0
- package/shared/agent-avatars/m2.png +0 -0
- package/shared/agent-avatars/m3.png +0 -0
- package/shared/agent-avatars/m4.png +0 -0
- package/shared/agent-avatars/m5.png +0 -0
- package/shared/agent-avatars/m6.png +0 -0
- package/shared/agent-avatars/marie.png +0 -0
- package/shared/agent-avatars/marilyn.png +0 -0
- package/shared/agent-avatars/neil.png +0 -0
- package/shared/agent-avatars/nikola.png +0 -0
- package/shared/agent-avatars/nina.png +0 -0
- package/shared/agent-avatars/paul.png +0 -0
- package/shared/agent-avatars/ringo.png +0 -0
- package/shared/agent-avatars/rosie.png +0 -0
- package/shared/agent-avatars/ste.png +0 -0
- package/shared/agent-avatars/steve.png +0 -0
- package/shared/agent-avatars/sun.png +0 -0
- package/shared/agent-avatars/tom.png +0 -0
- package/shared/agent-avatars/warren.png +0 -0
- package/shared/agent-avatars/woz.png +0 -0
- package/shared/agent-avatars/zaha.png +0 -0
- package/workframe-api/Dockerfile +14 -0
- package/workframe-api/README.md +28 -0
- package/workframe-api/action_proxy.py +131 -0
- package/workframe-api/auth_rate_limit.py +49 -0
- package/workframe-api/catalog/avatar-catalog.json +171 -0
- package/workframe-api/catalog/logo-catalog.json +86 -0
- package/workframe-api/catalog/user-avatar-catalog.json +171 -0
- package/workframe-api/credential_vault.py +445 -0
- package/workframe-api/data/.gitkeep +0 -0
- package/workframe-api/data/avatar-catalog.json +41 -0
- package/workframe-api/data/logo-catalog.json +14 -0
- package/workframe-api/data/user-avatar-catalog.json +18 -0
- package/workframe-api/email_sender.py +220 -0
- package/workframe-api/google_auth.py +90 -0
- package/workframe-api/install_api.py +359 -0
- package/workframe-api/internal_proxy_auth.py +150 -0
- package/workframe-api/llm_proxy.py +277 -0
- package/workframe-api/oidc_jwt.py +108 -0
- package/workframe-api/package.json +13 -0
- package/workframe-api/platform_auth.py +194 -0
- package/workframe-api/profile_secret_policy.py +86 -0
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -0
- package/workframe-api/public/assets/index-DYnLrCZZ.js +9 -0
- package/workframe-api/public/assets/index-DglUqFB_.js +9 -0
- package/workframe-api/public/index.html +12 -0
- package/workframe-api/requirements.txt +2 -0
- package/workframe-api/server.py +19646 -0
- package/workframe-api/site_meta.py +271 -0
- package/workframe-api/stack_config.py +427 -0
- package/workframe-api/tests/__init__.py +0 -0
- package/workframe-api/tests/db_setup.py +13 -0
- package/workframe-api/tests/test_admin_updates_gated.py +30 -0
- package/workframe-api/tests/test_agent_dm_bootstrap.py +196 -0
- package/workframe-api/tests/test_agent_profile_sync.py +76 -0
- package/workframe-api/tests/test_auth_email.py +222 -0
- package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +99 -0
- package/workframe-api/tests/test_auth_rate_limit.py +19 -0
- package/workframe-api/tests/test_avatar_resolve.py +77 -0
- package/workframe-api/tests/test_child_soul_template.py +71 -0
- package/workframe-api/tests/test_credential_canary.py +135 -0
- package/workframe-api/tests/test_credential_isolation.py +448 -0
- package/workframe-api/tests/test_credential_resolution.py +206 -0
- package/workframe-api/tests/test_device_oauth.py +108 -0
- package/workframe-api/tests/test_doctor_repair.py +103 -0
- package/workframe-api/tests/test_ensure_profile_api.py +77 -0
- package/workframe-api/tests/test_gateway_compose_security.py +136 -0
- package/workframe-api/tests/test_install_secure_host.py +39 -0
- package/workframe-api/tests/test_internal_proxy_auth.py +125 -0
- package/workframe-api/tests/test_invite_runtime_bootstrap.py +72 -0
- package/workframe-api/tests/test_kanban_delegation.py +185 -0
- package/workframe-api/tests/test_llm_proxy.py +155 -0
- package/workframe-api/tests/test_login_access_policy.py +183 -0
- package/workframe-api/tests/test_mvp_model_bootstrap.py +75 -0
- package/workframe-api/tests/test_onboarding_bootstrap.py +248 -0
- package/workframe-api/tests/test_platform_auth.py +47 -0
- package/workframe-api/tests/test_profile_config_path.py +56 -0
- package/workframe-api/tests/test_profile_config_yaml_repair.py +63 -0
- package/workframe-api/tests/test_profile_create.py +72 -0
- package/workframe-api/tests/test_profile_identity_overlay.py +61 -0
- package/workframe-api/tests/test_profile_install_health.py +45 -0
- package/workframe-api/tests/test_profile_secret_policy.py +57 -0
- package/workframe-api/tests/test_profile_workspace_cwd.py +34 -0
- package/workframe-api/tests/test_provider_bootstrap.py +75 -0
- package/workframe-api/tests/test_provider_connect.py +54 -0
- package/workframe-api/tests/test_room_crud.py +192 -0
- package/workframe-api/tests/test_room_tenancy.py +701 -0
- package/workframe-api/tests/test_runtime_identity_backfill.py +34 -0
- package/workframe-api/tests/test_site_meta.py +81 -0
- package/workframe-api/tests/test_soul_stub.py +42 -0
- package/workframe-api/tests/test_space_member_sync.py +99 -0
- package/workframe-api/tests/test_stripe_stack_config.py +37 -0
- package/workframe-api/tests/test_supervisor_lifecycle.py +52 -0
- package/workframe-api/tests/test_turn_credential_vault.py +125 -0
- package/workframe-api/tests/test_updates.py +176 -0
- package/workframe-api/tests/test_user_cohort.py +113 -0
- package/workframe-api/tests/test_vault_envelope.py +110 -0
- package/workframe-api/tests/test_workspace_members.py +183 -0
- package/workframe-api/tests/test_workspace_messaging_sync.py +125 -0
- package/workframe-api/tests/test_workspace_provider_list.py +57 -0
- package/workframe-api/time-bind-chat.py +99 -0
- package/workframe-api/turn_credentials.py +226 -0
- package/workframe-api/updates.py +417 -0
- package/workframe-api/vault_kek.py +159 -0
- package/workframe-api/zk_auth.py +633 -0
- package/workframe-supervisor/Dockerfile +11 -0
- package/workframe-supervisor/profile_secret_policy.py +76 -0
- package/workframe-supervisor/server.py +787 -0
- package/workframe-supervisor/tests/test_exec_guard.py +42 -0
- package/workframe-supervisor/tests/test_server_import.py +21 -0
- package/workframe-ui/docker/nginx.conf +85 -0
- package/workframe-ui/public/assets/1-DLJbBkOb.png +0 -0
- package/workframe-ui/public/assets/10-uwRwj5ce.png +0 -0
- package/workframe-ui/public/assets/11-5OuV9F_e.png +0 -0
- package/workframe-ui/public/assets/12-u_axjxW-.png +0 -0
- package/workframe-ui/public/assets/13-ldSvcMsH.png +0 -0
- package/workframe-ui/public/assets/14-xdcALEYD.png +0 -0
- package/workframe-ui/public/assets/15-aZ4snEFB.png +0 -0
- package/workframe-ui/public/assets/16-L_5-DttY.png +0 -0
- package/workframe-ui/public/assets/2-zOPZTppD.png +0 -0
- package/workframe-ui/public/assets/3-Dc3WoVu5.png +0 -0
- package/workframe-ui/public/assets/4-C50hk7_m.png +0 -0
- package/workframe-ui/public/assets/5-Eweetkq4.png +0 -0
- package/workframe-ui/public/assets/6-5sOXgfkw.png +0 -0
- package/workframe-ui/public/assets/7-BqRBCbiC.png +0 -0
- package/workframe-ui/public/assets/8-DEDKS94h.png +0 -0
- package/workframe-ui/public/assets/9-DNj34GW-.png +0 -0
- package/workframe-ui/public/assets/ada-DsvuOc9n.png +0 -0
- package/workframe-ui/public/assets/aibert-BCz8Lo8H.png +0 -0
- package/workframe-ui/public/assets/amelia-DUf3EBGu.png +0 -0
- package/workframe-ui/public/assets/andy-Cpymuhhx.png +0 -0
- package/workframe-ui/public/assets/arc-CBDYvkAF.js +1 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +1 -0
- package/workframe-ui/public/assets/architectureDiagram-3BPJPVTR-XnBRKeW0.js +36 -0
- package/workframe-ui/public/assets/array-BifhSqXX.js +1 -0
- package/workframe-ui/public/assets/avatars/ada.png +0 -0
- package/workframe-ui/public/assets/avatars/aibert.png +0 -0
- package/workframe-ui/public/assets/avatars/amelia.png +0 -0
- package/workframe-ui/public/assets/avatars/andy.png +0 -0
- package/workframe-ui/public/assets/avatars/bob.png +0 -0
- package/workframe-ui/public/assets/avatars/buzz.png +0 -0
- package/workframe-ui/public/assets/avatars/carl.png +0 -0
- package/workframe-ui/public/assets/avatars/catalog.json +171 -0
- package/workframe-ui/public/assets/avatars/corbu.png +0 -0
- package/workframe-ui/public/assets/avatars/diana.png +0 -0
- package/workframe-ui/public/assets/avatars/elvis.png +0 -0
- package/workframe-ui/public/assets/avatars/frida.png +0 -0
- package/workframe-ui/public/assets/avatars/george.png +0 -0
- package/workframe-ui/public/assets/avatars/grace.png +0 -0
- package/workframe-ui/public/assets/avatars/hedy.png +0 -0
- package/workframe-ui/public/assets/avatars/hermes.png +0 -0
- package/workframe-ui/public/assets/avatars/isaac.png +0 -0
- package/workframe-ui/public/assets/avatars/john.png +0 -0
- package/workframe-ui/public/assets/avatars/joni.png +0 -0
- package/workframe-ui/public/assets/avatars/leo.png +0 -0
- package/workframe-ui/public/assets/avatars/louis.png +0 -0
- package/workframe-ui/public/assets/avatars/ludwig.png +0 -0
- package/workframe-ui/public/assets/avatars/marie.png +0 -0
- package/workframe-ui/public/assets/avatars/marilyn.png +0 -0
- package/workframe-ui/public/assets/avatars/nikola.png +0 -0
- package/workframe-ui/public/assets/avatars/nina.png +0 -0
- package/workframe-ui/public/assets/avatars/paul.png +0 -0
- package/workframe-ui/public/assets/avatars/ringo.png +0 -0
- package/workframe-ui/public/assets/avatars/rosie.png +0 -0
- package/workframe-ui/public/assets/avatars/steve.png +0 -0
- package/workframe-ui/public/assets/avatars/sun.png +0 -0
- package/workframe-ui/public/assets/avatars/warren.png +0 -0
- package/workframe-ui/public/assets/avatars/woz.png +0 -0
- package/workframe-ui/public/assets/avatars/zaha.png +0 -0
- package/workframe-ui/public/assets/blockDiagram-GPEHLZMM-VYHUfVhd.js +132 -0
- package/workframe-ui/public/assets/bob-DRz-48Id.png +0 -0
- package/workframe-ui/public/assets/branding/banner.png +0 -0
- package/workframe-ui/public/assets/branding/og-default.png +0 -0
- package/workframe-ui/public/assets/branding/workframe'white.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-1.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-2.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-3.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-4.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-5.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-banner.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-horizontal-mini.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-horizontal-nano.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-horizontal.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-vertical-alt.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-vertical.png +0 -0
- package/workframe-ui/public/assets/branding/workframe.png +0 -0
- package/workframe-ui/public/assets/buzz-mC4PtMvC.png +0 -0
- package/workframe-ui/public/assets/c4Diagram-AAUBKEIU-BTjUcJpm.js +10 -0
- package/workframe-ui/public/assets/carl-CtE74db_.png +0 -0
- package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +1 -0
- package/workframe-ui/public/assets/chunk-2J33WTMH-w7uu7R-b.js +1 -0
- package/workframe-ui/public/assets/chunk-3OPIFGDE-Cb9LtnDX.js +62 -0
- package/workframe-ui/public/assets/chunk-4BX2VUAB-DiQ-qCwH.js +1 -0
- package/workframe-ui/public/assets/chunk-55IACEB6-C-mLFr7z.js +1 -0
- package/workframe-ui/public/assets/chunk-5ZQYHXKU-DOesfiCI.js +2 -0
- package/workframe-ui/public/assets/chunk-727SXJPM-BJ3oBZuz.js +206 -0
- package/workframe-ui/public/assets/chunk-AQP2D5EJ-CCA6xpGs.js +231 -0
- package/workframe-ui/public/assets/chunk-BSJP7CBP-a0cMNFb2.js +1 -0
- package/workframe-ui/public/assets/chunk-CSCIHK7Q-kuqN8EIY.js +122 -0
- package/workframe-ui/public/assets/chunk-FMBD7UC4-DyPgYHCg.js +15 -0
- package/workframe-ui/public/assets/chunk-KSCS5N6A-CdUuvR0V.js +10 -0
- package/workframe-ui/public/assets/chunk-L5ZTLDWV-Dq9NoWmK.js +1 -0
- package/workframe-ui/public/assets/chunk-LZXEDZCA-p74rddlO.js +2 -0
- package/workframe-ui/public/assets/chunk-ND2GUHAM-DBD2u1Gz.js +1 -0
- package/workframe-ui/public/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/workframe-ui/public/assets/chunk-NZK2D7GU-BeIeYFnd.js +1 -0
- package/workframe-ui/public/assets/chunk-O5CBEL6O-ClHc56ib.js +70 -0
- package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +1 -0
- package/workframe-ui/public/assets/chunk-XPW4576I-EFr8R_1p.js +32 -0
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +1 -0
- package/workframe-ui/public/assets/corbu-KiaMXzXQ.png +0 -0
- package/workframe-ui/public/assets/cose-bilkent-S5V4N54A-C7aPBODd.js +1 -0
- package/workframe-ui/public/assets/cytoscape.esm-h6BdjjI9.js +321 -0
- package/workframe-ui/public/assets/dagre-BM42HDAG-BdU1Rv-H.js +4 -0
- package/workframe-ui/public/assets/dagre-Bx709z4p.js +1 -0
- package/workframe-ui/public/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/workframe-ui/public/assets/diagram-2AECGRRQ-DWowSo85.js +43 -0
- package/workframe-ui/public/assets/diagram-5GNKFQAL-MnxBbceO.js +10 -0
- package/workframe-ui/public/assets/diagram-KO2AKTUF-DQaLRXFf.js +3 -0
- package/workframe-ui/public/assets/diagram-LMA3HP47-CQaBud9k.js +24 -0
- package/workframe-ui/public/assets/diagram-OG6HWLK6-D8bAXbY9.js +24 -0
- package/workframe-ui/public/assets/diana-DW0MsL38.png +0 -0
- package/workframe-ui/public/assets/dist-DGpTLHr_.js +1 -0
- package/workframe-ui/public/assets/elvis-LCFaZIcT.png +0 -0
- package/workframe-ui/public/assets/erDiagram-TEJ5UH35-1E-xSvBK.js +85 -0
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +1 -0
- package/workframe-ui/public/assets/flowDiagram-I6XJVG4X-CgOVD5hu.js +162 -0
- package/workframe-ui/public/assets/frida-CXFA0w3F.png +0 -0
- package/workframe-ui/public/assets/ganttDiagram-6RSMTGT7-JFYAIauo.js +292 -0
- package/workframe-ui/public/assets/george-DBSH2Sm2.png +0 -0
- package/workframe-ui/public/assets/gitGraph-WXDBUCRP-B9REenIl.js +1 -0
- package/workframe-ui/public/assets/gitGraphDiagram-PVQCEYII-BQ7NcMSn.js +106 -0
- package/workframe-ui/public/assets/grace-BhV0UPc0.png +0 -0
- package/workframe-ui/public/assets/graphlib-B8gBHxth.js +1 -0
- package/workframe-ui/public/assets/hedy-BR2IHift.png +0 -0
- package/workframe-ui/public/assets/hermes-CqCzcE0y.png +0 -0
- package/workframe-ui/public/assets/index-Dnw6vjqb.js +133 -0
- package/workframe-ui/public/assets/index-DpAGxump.css +1 -0
- package/workframe-ui/public/assets/info-J43DQDTF-CL6-eTjH.js +1 -0
- package/workframe-ui/public/assets/infoDiagram-5YYISTIA-LJTODW4W.js +2 -0
- package/workframe-ui/public/assets/init-D6jRqBbL.js +1 -0
- package/workframe-ui/public/assets/isaac-D1nhJAuv.png +0 -0
- package/workframe-ui/public/assets/ishikawaDiagram-YF4QCWOH-bchrQVuo.js +70 -0
- package/workframe-ui/public/assets/john-zSPWwNi4.png +0 -0
- package/workframe-ui/public/assets/joni-BFLoyfJP.png +0 -0
- package/workframe-ui/public/assets/journeyDiagram-JHISSGLW-DkrvYuxP.js +139 -0
- package/workframe-ui/public/assets/kanban-definition-UN3LZRKU-DFRbj0IG.js +89 -0
- package/workframe-ui/public/assets/katex-Vhh-h91d.js +257 -0
- package/workframe-ui/public/assets/leo-C_3IOL11.png +0 -0
- package/workframe-ui/public/assets/line-Vd48P7-O.js +1 -0
- package/workframe-ui/public/assets/linear-Ckizh2G7.js +1 -0
- package/workframe-ui/public/assets/louis-DEEECFSX.png +0 -0
- package/workframe-ui/public/assets/ludwig-_hoKhhyK.png +0 -0
- package/workframe-ui/public/assets/marie-DET6MsfO.png +0 -0
- package/workframe-ui/public/assets/marilyn-DTqwt8Yh.png +0 -0
- package/workframe-ui/public/assets/mermaid-parser.core-Bkimsnqj.js +4 -0
- package/workframe-ui/public/assets/mermaid.core-x0TvVuPo.js +9 -0
- package/workframe-ui/public/assets/mindmap-definition-RKZ34NQL-6ykAFPEz.js +96 -0
- package/workframe-ui/public/assets/nikola-B4PtHrJv.png +0 -0
- package/workframe-ui/public/assets/nina-BYbrOn0d.png +0 -0
- package/workframe-ui/public/assets/ordinal-hYBb2elL.js +1 -0
- package/workframe-ui/public/assets/packet-YPE3B663-Dw3xgMDt.js +1 -0
- package/workframe-ui/public/assets/path-BWPyau1x.js +1 -0
- package/workframe-ui/public/assets/paul-CGURYQIn.png +0 -0
- package/workframe-ui/public/assets/pie-LRSECV5Y-DATysawG.js +1 -0
- package/workframe-ui/public/assets/pieDiagram-4H26LBE5-SJKD1S0S.js +30 -0
- package/workframe-ui/public/assets/project-logos/1.png +0 -0
- package/workframe-ui/public/assets/project-logos/10.png +0 -0
- package/workframe-ui/public/assets/project-logos/11.png +0 -0
- package/workframe-ui/public/assets/project-logos/12.png +0 -0
- package/workframe-ui/public/assets/project-logos/13.png +0 -0
- package/workframe-ui/public/assets/project-logos/14.png +0 -0
- package/workframe-ui/public/assets/project-logos/15.png +0 -0
- package/workframe-ui/public/assets/project-logos/16.png +0 -0
- package/workframe-ui/public/assets/project-logos/2.png +0 -0
- package/workframe-ui/public/assets/project-logos/3.png +0 -0
- package/workframe-ui/public/assets/project-logos/4.png +0 -0
- package/workframe-ui/public/assets/project-logos/5.png +0 -0
- package/workframe-ui/public/assets/project-logos/6.png +0 -0
- package/workframe-ui/public/assets/project-logos/7.png +0 -0
- package/workframe-ui/public/assets/project-logos/8.png +0 -0
- package/workframe-ui/public/assets/project-logos/9.png +0 -0
- package/workframe-ui/public/assets/project-logos/catalog.json +86 -0
- package/workframe-ui/public/assets/quadrantDiagram-W4KKPZXB-BrYDZX8q.js +7 -0
- package/workframe-ui/public/assets/radar-GUYGQ44K-BmWYPCds.js +1 -0
- package/workframe-ui/public/assets/requirementDiagram-4Y6WPE33-DwL9Mc8e.js +84 -0
- package/workframe-ui/public/assets/ringo-WhfUNOyY.png +0 -0
- package/workframe-ui/public/assets/rosie-CAtcIf87.png +0 -0
- package/workframe-ui/public/assets/rough.esm-CSKSodPl.js +1 -0
- package/workframe-ui/public/assets/sankeyDiagram-5OEKKPKP-DYIFsL8h.js +40 -0
- package/workframe-ui/public/assets/sequenceDiagram-3UESZ5HK-0-FPkFk8.js +162 -0
- package/workframe-ui/public/assets/src-B_od6b6h.js +1 -0
- package/workframe-ui/public/assets/stateDiagram-AJRCARHV-BQCiBk6u.js +1 -0
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +1 -0
- package/workframe-ui/public/assets/steve-CgXXJ9EZ.png +0 -0
- package/workframe-ui/public/assets/sun-BLNAhoZd.png +0 -0
- package/workframe-ui/public/assets/timeline-definition-PNZ67QCA-DS3tFcXj.js +120 -0
- package/workframe-ui/public/assets/treeView-BLDUP644-DSyUCKLY.js +1 -0
- package/workframe-ui/public/assets/treemap-LRROVOQU-CEZaNh5Y.js +1 -0
- package/workframe-ui/public/assets/vennDiagram-CIIHVFJN-CD-Vc9NF.js +34 -0
- package/workframe-ui/public/assets/wardley-L42UT6IY-Drq5w1Mc.js +1 -0
- package/workframe-ui/public/assets/wardleyDiagram-YWT4CUSO-DouXDJoF.js +78 -0
- package/workframe-ui/public/assets/warren-DIH7UKMY.png +0 -0
- package/workframe-ui/public/assets/woz-D2yleG-V.png +0 -0
- package/workframe-ui/public/assets/xychartDiagram-2RQKCTM6-DDf_Lol5.js +7 -0
- package/workframe-ui/public/assets/zaha-wersOEq9.png +0 -0
- package/workframe-ui/public/favicon.ico +0 -0
- package/workframe-ui/public/favicon.svg +7 -0
- package/workframe-ui/public/icons.svg +24 -0
- package/workframe-ui/public/index.html +50 -0
- package/workframe-ui/public/manifest.webmanifest +18 -0
- package/workframe-ui/public/workframe-config.json +4 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Email sender module for Workframe API.
|
|
3
|
+
Sends OTP verification emails via SMTP (env or stack_config).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import smtplib
|
|
10
|
+
from email.mime.multipart import MIMEMultipart
|
|
11
|
+
from email.mime.text import MIMEText
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import stack_config
|
|
15
|
+
|
|
16
|
+
APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://127.0.0.1:18644").rstrip("/")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _tls_flags(cfg: dict[str, Any]) -> tuple[bool, bool]:
|
|
20
|
+
port = int(cfg.get("port") or 587)
|
|
21
|
+
secure = stack_config.normalize_smtp_secure(port, str(cfg.get("secure") or ""))
|
|
22
|
+
use_ssl = secure == "ssl"
|
|
23
|
+
use_tls = secure == "starttls"
|
|
24
|
+
return use_ssl, use_tls
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _smtp_send(msg: MIMEMultipart, to_email: str, cfg: dict[str, Any]) -> None:
|
|
28
|
+
host = str(cfg.get("host") or "").strip()
|
|
29
|
+
if not host:
|
|
30
|
+
raise RuntimeError("SMTP_HOST not configured")
|
|
31
|
+
port = int(cfg.get("port") or 587)
|
|
32
|
+
user = str(cfg.get("user") or "").strip()
|
|
33
|
+
password = str(cfg.get("password") or "").strip().replace(" ", "")
|
|
34
|
+
from_addr = str(cfg.get("from") or user or "").strip()
|
|
35
|
+
if not from_addr:
|
|
36
|
+
raise RuntimeError("SMTP from address not configured")
|
|
37
|
+
if user and not password:
|
|
38
|
+
raise RuntimeError("SMTP password is required when SMTP user is set")
|
|
39
|
+
msg["From"] = from_addr
|
|
40
|
+
use_ssl, use_tls = _tls_flags(cfg)
|
|
41
|
+
try:
|
|
42
|
+
if use_ssl:
|
|
43
|
+
server = smtplib.SMTP_SSL(host, port, timeout=30)
|
|
44
|
+
else:
|
|
45
|
+
server = smtplib.SMTP(host, port, timeout=30)
|
|
46
|
+
with server:
|
|
47
|
+
server.ehlo()
|
|
48
|
+
if use_tls and not use_ssl:
|
|
49
|
+
server.starttls()
|
|
50
|
+
server.ehlo()
|
|
51
|
+
if user:
|
|
52
|
+
server.login(user, password)
|
|
53
|
+
server.sendmail(from_addr, [to_email], msg.as_string())
|
|
54
|
+
except smtplib.SMTPAuthenticationError as exc:
|
|
55
|
+
raise RuntimeError(f"SMTP login failed: {exc}") from exc
|
|
56
|
+
except smtplib.SMTPSenderRefused as exc:
|
|
57
|
+
sender = str(getattr(exc, "sender", "") or from_addr)
|
|
58
|
+
if user and sender.lower() != user.lower():
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"SMTP rejected From address {sender!r} for login {user!r}"
|
|
61
|
+
) from exc
|
|
62
|
+
raise RuntimeError(f"SMTP error: {exc}") from exc
|
|
63
|
+
except smtplib.SMTPException as exc:
|
|
64
|
+
raise RuntimeError(f"SMTP error: {exc}") from exc
|
|
65
|
+
except OSError as exc:
|
|
66
|
+
raise RuntimeError(f"Network error sending email: {exc}") from exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _active_smtp() -> dict[str, Any]:
|
|
70
|
+
cfg = stack_config.resolved_smtp()
|
|
71
|
+
if cfg.get("source") == "none":
|
|
72
|
+
return {}
|
|
73
|
+
return cfg
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def send_email_with_config(
|
|
77
|
+
to_email: str,
|
|
78
|
+
subject: str,
|
|
79
|
+
body_text: str,
|
|
80
|
+
body_html: str = "",
|
|
81
|
+
cfg: dict[str, Any] | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
smtp = cfg or _active_smtp()
|
|
84
|
+
if not smtp.get("host"):
|
|
85
|
+
raise RuntimeError("SMTP_HOST not configured")
|
|
86
|
+
msg = MIMEMultipart("alternative")
|
|
87
|
+
msg["Subject"] = subject
|
|
88
|
+
msg["To"] = to_email
|
|
89
|
+
msg.attach(MIMEText(body_text, "plain"))
|
|
90
|
+
if body_html:
|
|
91
|
+
msg.attach(MIMEText(body_html, "html"))
|
|
92
|
+
_smtp_send(msg, to_email, smtp)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _branded_html(
|
|
96
|
+
*,
|
|
97
|
+
brand: str,
|
|
98
|
+
headline: str,
|
|
99
|
+
intro: str,
|
|
100
|
+
body_html: str,
|
|
101
|
+
logo_url: str = "",
|
|
102
|
+
footer: str = "If you didn't request this, you can safely ignore this email.",
|
|
103
|
+
) -> str:
|
|
104
|
+
logo_block = (
|
|
105
|
+
f'<img src="{logo_url}" alt="" style="height:40px;margin-bottom:12px;" />'
|
|
106
|
+
if logo_url
|
|
107
|
+
else ""
|
|
108
|
+
)
|
|
109
|
+
return f"""\
|
|
110
|
+
<html>
|
|
111
|
+
<body style="margin:0; background:#f6f7fb; font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color:#1d2340;">
|
|
112
|
+
<div style="max-width:560px; margin:0 auto; padding:32px 20px;">
|
|
113
|
+
<div style="background:#ffffff; border:1px solid #e4e7f2; border-radius:20px; padding:32px; box-shadow:0 24px 64px rgba(14,20,49,0.08);">
|
|
114
|
+
{logo_block}
|
|
115
|
+
<div style="font-size:12px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:#6c5ce7; margin-bottom:12px;">{brand}</div>
|
|
116
|
+
<h1 style="margin:0 0 12px; font-size:28px; line-height:1.2;">{headline}</h1>
|
|
117
|
+
<p style="margin:0 0 24px; color:#4f5878; font-size:16px; line-height:1.6;">{intro}</p>
|
|
118
|
+
{body_html}
|
|
119
|
+
<p style="margin:24px 0 0; color:#7b849d; font-size:12px; line-height:1.6;">{footer}</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _code_block_html(code: str) -> str:
|
|
128
|
+
return f"""\
|
|
129
|
+
<div style="background:linear-gradient(180deg, #f5f7ff 0%, #eef1ff 100%); border:1px solid #d9defa; border-radius:16px; padding:18px 20px; text-align:center; margin-bottom:20px;">
|
|
130
|
+
<div style="font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:#6b7280; margin-bottom:10px;">Verification code</div>
|
|
131
|
+
<div style="font-size:34px; font-weight:800; letter-spacing:10px; color:#111827; font-variant-numeric:tabular-nums;">{code}</div>
|
|
132
|
+
</div>
|
|
133
|
+
<p style="margin:0 0 20px; color:#4f5878; font-size:14px; line-height:1.6;">This code expires in 10 minutes.</p>
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _cta_button_html(url: str, label: str) -> str:
|
|
138
|
+
return (
|
|
139
|
+
f'<a href="{url}" style="display:inline-block; padding:12px 20px; background:#6c5ce7; '
|
|
140
|
+
f'color:#ffffff; text-decoration:none; border-radius:10px; font-weight:700;">{label}</a>'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_verification_email(
|
|
145
|
+
to_email: str,
|
|
146
|
+
code: str,
|
|
147
|
+
verification_url: str,
|
|
148
|
+
workspace_name: str = "Workframe",
|
|
149
|
+
logo_url: str = "",
|
|
150
|
+
) -> MIMEMultipart:
|
|
151
|
+
msg = MIMEMultipart("alternative")
|
|
152
|
+
brand = workspace_name or "Workframe"
|
|
153
|
+
msg["Subject"] = f"Your {brand} sign-in code"
|
|
154
|
+
msg["To"] = to_email
|
|
155
|
+
text_body = f"""\
|
|
156
|
+
{brand} sign-in code
|
|
157
|
+
|
|
158
|
+
Your verification code is:
|
|
159
|
+
|
|
160
|
+
{code}
|
|
161
|
+
|
|
162
|
+
This code expires in 10 minutes.
|
|
163
|
+
|
|
164
|
+
Or click the link to verify:
|
|
165
|
+
{verification_url}
|
|
166
|
+
|
|
167
|
+
If you didn't request this, ignore this email.
|
|
168
|
+
"""
|
|
169
|
+
body_html = _code_block_html(code) + _cta_button_html(verification_url, "Verify now")
|
|
170
|
+
html_body = _branded_html(
|
|
171
|
+
brand=brand,
|
|
172
|
+
headline="Sign in with your code",
|
|
173
|
+
intro="Use this one-time code to finish signing in to Workframe.",
|
|
174
|
+
body_html=body_html,
|
|
175
|
+
logo_url=logo_url,
|
|
176
|
+
)
|
|
177
|
+
msg.attach(MIMEText(text_body, "plain"))
|
|
178
|
+
msg.attach(MIMEText(html_body, "html"))
|
|
179
|
+
return msg
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def send_verification_email(
|
|
183
|
+
to_email: str,
|
|
184
|
+
code: str,
|
|
185
|
+
verification_url: str,
|
|
186
|
+
workspace_name: str = "",
|
|
187
|
+
logo_url: str = "",
|
|
188
|
+
) -> None:
|
|
189
|
+
msg = _build_verification_email(to_email, code, verification_url, workspace_name, logo_url)
|
|
190
|
+
_smtp_send(msg, to_email, _active_smtp())
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def send_email(to_email: str, subject: str, body_text: str, body_html: str = "") -> None:
|
|
194
|
+
send_email_with_config(to_email, subject, body_text, body_html)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def send_branded_invite_email(
|
|
198
|
+
to_email: str,
|
|
199
|
+
workspace_name: str,
|
|
200
|
+
invite_url: str,
|
|
201
|
+
logo_url: str = "",
|
|
202
|
+
) -> None:
|
|
203
|
+
brand = workspace_name or "Workframe"
|
|
204
|
+
subject = f"Join {brand} on Workframe"
|
|
205
|
+
text = f"You were invited to {brand}.\n\nAccept: {invite_url}\n"
|
|
206
|
+
body_html = _cta_button_html(invite_url, "Accept invite")
|
|
207
|
+
html = _branded_html(
|
|
208
|
+
brand=brand,
|
|
209
|
+
headline=f"Join {brand}",
|
|
210
|
+
intro="You were invited to collaborate on Workframe.",
|
|
211
|
+
body_html=body_html,
|
|
212
|
+
logo_url=logo_url,
|
|
213
|
+
footer="If you weren't expecting this invite, you can ignore this email.",
|
|
214
|
+
)
|
|
215
|
+
send_email_with_config(to_email, subject, text, html)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
html = _build_verification_email("test@example.com", "123456", "http://127.0.0.1:18644/?code=123456").as_string()
|
|
220
|
+
assert "Verification code" in html and "Verify now" in html
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Google Sign-In for Workframe (workspace-configured OAuth app)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import oidc_jwt
|
|
13
|
+
import stack_config
|
|
14
|
+
|
|
15
|
+
APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://127.0.0.1:18644").rstrip("/")
|
|
16
|
+
_PENDING: dict[str, dict[str, str]] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _google_creds() -> tuple[str, str]:
|
|
20
|
+
cfg = stack_config.resolved_google_oauth()
|
|
21
|
+
client_id = str(cfg.get("client_id") or "").strip()
|
|
22
|
+
client_secret = str(cfg.get("client_secret") or "").strip()
|
|
23
|
+
if not client_id or not client_secret:
|
|
24
|
+
raise ValueError("Google Sign-In is not configured")
|
|
25
|
+
return client_id, client_secret
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def start_google_auth(invite_email: str = "", invite_token: str = "") -> dict[str, Any]:
|
|
29
|
+
client_id, _ = _google_creds()
|
|
30
|
+
state = secrets.token_urlsafe(24)
|
|
31
|
+
_PENDING[state] = {
|
|
32
|
+
"invite_email": invite_email.strip().lower(),
|
|
33
|
+
"invite_token": invite_token.strip(),
|
|
34
|
+
}
|
|
35
|
+
redirect_uri = f"{APP_BASE_URL}/api/auth/google/callback"
|
|
36
|
+
params = {
|
|
37
|
+
"client_id": client_id,
|
|
38
|
+
"redirect_uri": redirect_uri,
|
|
39
|
+
"response_type": "code",
|
|
40
|
+
"scope": "openid email profile",
|
|
41
|
+
"state": state,
|
|
42
|
+
"access_type": "online",
|
|
43
|
+
"prompt": "select_account",
|
|
44
|
+
}
|
|
45
|
+
if invite_email:
|
|
46
|
+
params["login_hint"] = invite_email.strip()
|
|
47
|
+
url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode(params)
|
|
48
|
+
return {"ok": True, "auth_url": url, "state": state}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def exchange_google_code(code: str, state: str) -> dict[str, Any]:
|
|
52
|
+
pending = _PENDING.pop(state, None)
|
|
53
|
+
if not pending:
|
|
54
|
+
raise ValueError("invalid or expired OAuth state")
|
|
55
|
+
client_id, client_secret = _google_creds()
|
|
56
|
+
redirect_uri = f"{APP_BASE_URL}/api/auth/google/callback"
|
|
57
|
+
data = urllib.parse.urlencode(
|
|
58
|
+
{
|
|
59
|
+
"code": code,
|
|
60
|
+
"client_id": client_id,
|
|
61
|
+
"client_secret": client_secret,
|
|
62
|
+
"redirect_uri": redirect_uri,
|
|
63
|
+
"grant_type": "authorization_code",
|
|
64
|
+
}
|
|
65
|
+
).encode()
|
|
66
|
+
req = urllib.request.Request(
|
|
67
|
+
"https://oauth2.googleapis.com/token",
|
|
68
|
+
data=data,
|
|
69
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
70
|
+
method="POST",
|
|
71
|
+
)
|
|
72
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
73
|
+
token_payload = json.loads(resp.read().decode())
|
|
74
|
+
id_token = str(token_payload.get("id_token") or "")
|
|
75
|
+
if not id_token:
|
|
76
|
+
raise ValueError("Google did not return an id_token")
|
|
77
|
+
claims = oidc_jwt.verify_google_id_token(id_token, client_id)
|
|
78
|
+
email = str(claims.get("email") or "").strip().lower()
|
|
79
|
+
if not email:
|
|
80
|
+
raise ValueError("Google account has no email")
|
|
81
|
+
if claims.get("email_verified") is False:
|
|
82
|
+
raise ValueError("Google email is not verified")
|
|
83
|
+
invite_email = pending.get("invite_email") or ""
|
|
84
|
+
if invite_email and email != invite_email:
|
|
85
|
+
raise ValueError("Google email does not match the invited address")
|
|
86
|
+
return {
|
|
87
|
+
"email": email,
|
|
88
|
+
"display_name": str(claims.get("name") or email),
|
|
89
|
+
"invite_token": pending.get("invite_token") or "",
|
|
90
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Install / stack-setup API helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sqlite3
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import stack_config
|
|
15
|
+
from email_sender import send_email_with_config
|
|
16
|
+
|
|
17
|
+
HERMES_DATA = Path(os.environ.get("HERMES_DATA", "/opt/data"))
|
|
18
|
+
NATIVE_PROFILE = os.environ.get("WORKFRAME_NATIVE_PROFILE", "workframe-agent").strip() or "workframe-agent"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _user_count(db_path: str) -> int:
|
|
22
|
+
try:
|
|
23
|
+
conn = sqlite3.connect(db_path, timeout=2.0)
|
|
24
|
+
row = conn.execute("SELECT COUNT(*) FROM users").fetchone()
|
|
25
|
+
conn.close()
|
|
26
|
+
return int(row[0]) if row else 0
|
|
27
|
+
except (sqlite3.Error, OSError):
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def install_window_open(db_path: str) -> bool:
|
|
32
|
+
"""Open until operator marks install complete — users may exist mid-onboarding."""
|
|
33
|
+
del db_path # ponytail: reserved for future per-install DB path checks
|
|
34
|
+
return not bool(stack_config.get_stack_config().get("install_complete"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _hermes_native_present() -> bool:
|
|
38
|
+
for slug in (NATIVE_PROFILE, "workframe-agent"):
|
|
39
|
+
if not slug:
|
|
40
|
+
continue
|
|
41
|
+
prof_dir = HERMES_DATA / "profiles" / slug
|
|
42
|
+
if (prof_dir / "config.yaml").is_file() or (prof_dir / "profile.yaml").is_file():
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _setup_complete(db_path: str) -> bool:
|
|
48
|
+
try:
|
|
49
|
+
conn = sqlite3.connect(db_path, timeout=2.0)
|
|
50
|
+
row = conn.execute(
|
|
51
|
+
"SELECT COUNT(*) FROM agent_profiles WHERE deleted_at IS NULL"
|
|
52
|
+
).fetchone()
|
|
53
|
+
conn.close()
|
|
54
|
+
return bool(row and row[0] > 0)
|
|
55
|
+
except (sqlite3.Error, OSError):
|
|
56
|
+
return _hermes_native_present()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def install_status_payload(
|
|
60
|
+
deployment_mode: str,
|
|
61
|
+
secure_mode: bool,
|
|
62
|
+
dev_unsafe: bool,
|
|
63
|
+
db_path: str,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
hermes = _hermes_native_present()
|
|
66
|
+
setup = _setup_complete(db_path)
|
|
67
|
+
smtp_ok = stack_config.smtp_configured()
|
|
68
|
+
return {
|
|
69
|
+
"ok": True,
|
|
70
|
+
"phase": "ready" if hermes and setup else ("hermes" if not hermes else "workspace"),
|
|
71
|
+
"hermes_present": hermes,
|
|
72
|
+
"setup_complete": setup,
|
|
73
|
+
"api_ok": True,
|
|
74
|
+
"deployment_mode": deployment_mode,
|
|
75
|
+
"mode": "dev_unsafe" if dev_unsafe else ("secure" if secure_mode else "dev_unsafe"),
|
|
76
|
+
"smtp_configured": smtp_ok,
|
|
77
|
+
"install_complete": bool(stack_config.get_stack_config().get("install_complete")),
|
|
78
|
+
"install_window_open": install_window_open(db_path),
|
|
79
|
+
"native_profile": NATIVE_PROFILE,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def smtp_test_send(to_email: str) -> dict[str, Any]:
|
|
84
|
+
to_email = str(to_email or "").strip().lower()
|
|
85
|
+
if not to_email or "@" not in to_email:
|
|
86
|
+
raise ValueError("valid email required")
|
|
87
|
+
cfg = stack_config.resolved_smtp()
|
|
88
|
+
if not cfg.get("host"):
|
|
89
|
+
raise ValueError("SMTP is not configured yet")
|
|
90
|
+
subject = "Workframe test email"
|
|
91
|
+
text = "If you received this, your Workframe SMTP settings are working."
|
|
92
|
+
html = "<p>If you received this, your Workframe SMTP settings are working.</p>"
|
|
93
|
+
send_email_with_config(to_email, subject, text, html, cfg)
|
|
94
|
+
stack_config.patch_stack_config({"smtp": {"admin_email": to_email}})
|
|
95
|
+
stack_config.mark_smtp_tested()
|
|
96
|
+
return {"ok": True, "email_sent": True, "to": to_email}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _normalize_app_base_url(url: str) -> str:
|
|
100
|
+
"""Ensure app_base_url has a scheme for health checks and OAuth callbacks."""
|
|
101
|
+
u = str(url or "").strip().rstrip("/")
|
|
102
|
+
if not u:
|
|
103
|
+
return ""
|
|
104
|
+
if not u.lower().startswith(("http://", "https://")):
|
|
105
|
+
u = f"https://{u}"
|
|
106
|
+
return u
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _hostname_only(url: str) -> str:
|
|
110
|
+
u = _normalize_app_base_url(url)
|
|
111
|
+
if not u:
|
|
112
|
+
return ""
|
|
113
|
+
try:
|
|
114
|
+
return urllib.parse.urlparse(u).hostname or ""
|
|
115
|
+
except Exception:
|
|
116
|
+
return str(url or "").strip().lower()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def dns_record_name(hostname: str) -> str:
|
|
120
|
+
host = _hostname_only(hostname) or str(hostname or "").strip().lower().rstrip(".")
|
|
121
|
+
if not host:
|
|
122
|
+
return "@"
|
|
123
|
+
parts = host.split(".")
|
|
124
|
+
if len(parts) <= 2:
|
|
125
|
+
return "@"
|
|
126
|
+
return parts[0]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def apex_domain(hostname: str) -> str:
|
|
130
|
+
host = _hostname_only(hostname) or str(hostname or "").strip().lower().rstrip(".")
|
|
131
|
+
if not host:
|
|
132
|
+
return ""
|
|
133
|
+
parts = host.split(".")
|
|
134
|
+
if len(parts) <= 2:
|
|
135
|
+
return host
|
|
136
|
+
return ".".join(parts[-2:])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def detect_public_ipv4() -> str | None:
|
|
140
|
+
for url in ("https://api4.ipify.org", "https://ifconfig.me/ip"):
|
|
141
|
+
try:
|
|
142
|
+
with urllib.request.urlopen(url, timeout=4) as resp:
|
|
143
|
+
ip = resp.read().decode("utf-8", errors="replace").strip()
|
|
144
|
+
if ip and "." in ip:
|
|
145
|
+
return ip
|
|
146
|
+
except (urllib.error.URLError, OSError, TimeoutError, ValueError):
|
|
147
|
+
continue
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def publish_hints_payload(public_url: str) -> dict[str, Any]:
|
|
152
|
+
host = _hostname_only(public_url)
|
|
153
|
+
apex = apex_domain(host)
|
|
154
|
+
ui_port = int(os.environ.get("WORKFRAME_UI_PORT", "18644") or "18644")
|
|
155
|
+
public_ip = detect_public_ipv4()
|
|
156
|
+
project_root = (
|
|
157
|
+
os.environ.get("WORKFRAME_HOST_PROJECT_ROOT", "").strip()
|
|
158
|
+
or "/opt/workframe/ProjectX"
|
|
159
|
+
)
|
|
160
|
+
setup_command = (
|
|
161
|
+
f"sudo bash {project_root}/scripts/workframe/setup-public-https.sh {host} {ui_port}"
|
|
162
|
+
if host
|
|
163
|
+
else ""
|
|
164
|
+
)
|
|
165
|
+
dns_name = dns_record_name(host)
|
|
166
|
+
return {
|
|
167
|
+
"ok": True,
|
|
168
|
+
"hostname": host,
|
|
169
|
+
"apex_domain": apex,
|
|
170
|
+
"public_ipv4": public_ip,
|
|
171
|
+
"ui_port": ui_port,
|
|
172
|
+
"project_root": project_root,
|
|
173
|
+
"health_url": f"https://{host}/api/health" if host else "",
|
|
174
|
+
"dns": {
|
|
175
|
+
"type": "A",
|
|
176
|
+
"name": dns_name,
|
|
177
|
+
"value": public_ip or "",
|
|
178
|
+
"ttl": "600",
|
|
179
|
+
},
|
|
180
|
+
"dns_cname": {
|
|
181
|
+
"type": "CNAME",
|
|
182
|
+
"name": dns_name,
|
|
183
|
+
"value": apex,
|
|
184
|
+
"hint": "Optional: point the subdomain at your apex hostname instead of an IP. CNAME cannot target an IP address.",
|
|
185
|
+
}
|
|
186
|
+
if apex and dns_name != "@"
|
|
187
|
+
else None,
|
|
188
|
+
"registrar_links": [
|
|
189
|
+
{
|
|
190
|
+
"label": "GoDaddy DNS",
|
|
191
|
+
"url": f"https://dcc.godaddy.com/manage/{apex}/dns" if apex else "https://dcc.godaddy.com/control/portfolio",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"label": "Namecheap DNS",
|
|
195
|
+
"url": f"https://ap.www.namecheap.com/Domains/DomainControlPanel/{apex}/advancedns"
|
|
196
|
+
if apex
|
|
197
|
+
else "https://www.namecheap.com/domains/",
|
|
198
|
+
},
|
|
199
|
+
{"label": "Cloudflare", "url": "https://dash.cloudflare.com"},
|
|
200
|
+
],
|
|
201
|
+
"setup_command": setup_command,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _loopback_hostname(hostname: str) -> bool:
|
|
206
|
+
h = (hostname or "").strip().lower().rstrip(".")
|
|
207
|
+
return h in ("127.0.0.1", "localhost", "::1") or h.endswith(".localhost")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _fetch_health(url: str, timeout: float = 8) -> dict[str, Any]:
|
|
211
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
212
|
+
body = resp.read().decode("utf-8", errors="replace")
|
|
213
|
+
ok = resp.status == 200 and '"ok"' in body
|
|
214
|
+
return {"ok": ok, "status": resp.status, "checked_url": url}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _local_stack_health() -> dict[str, Any]:
|
|
218
|
+
ui_port = int(os.environ.get("WORKFRAME_UI_PORT", "18644") or "18644")
|
|
219
|
+
app_base = str(
|
|
220
|
+
stack_config.get_stack_config().get("app_base_url")
|
|
221
|
+
or os.environ.get("APP_BASE_URL", "")
|
|
222
|
+
or f"http://127.0.0.1:{ui_port}",
|
|
223
|
+
)
|
|
224
|
+
host_header = _hostname_only(app_base) or "127.0.0.1"
|
|
225
|
+
req = urllib.request.Request(
|
|
226
|
+
"http://workframe-ui/api/health",
|
|
227
|
+
headers={"Host": f"{host_header}:{ui_port}"},
|
|
228
|
+
)
|
|
229
|
+
try:
|
|
230
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
231
|
+
body = resp.read().decode("utf-8", errors="replace")
|
|
232
|
+
ok = resp.status == 200 and '"ok"' in body
|
|
233
|
+
return {"ok": ok, "status": resp.status, "local_ok": ok, "checked_url": req.full_url}
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
return {"local_ok": False, "error": str(exc)}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _url_test_hint(exc: Exception, *, local: dict[str, Any]) -> str:
|
|
239
|
+
msg = str(exc).lower()
|
|
240
|
+
ui_port = int(os.environ.get("WORKFRAME_UI_PORT", "18644") or "18644")
|
|
241
|
+
if "connection refused" in msg or "errno 111" in msg:
|
|
242
|
+
base = (
|
|
243
|
+
f"Nothing is listening on HTTPS for that host yet. Use step 2 (Set up HTTPS on this server), "
|
|
244
|
+
f"point DNS here, and open ports 80/443. Caddy proxies to 127.0.0.1:{ui_port}."
|
|
245
|
+
)
|
|
246
|
+
if local.get("local_ok"):
|
|
247
|
+
return f"Workframe is healthy on this server. {base}"
|
|
248
|
+
return base
|
|
249
|
+
if "timed out" in msg or "timeout" in msg:
|
|
250
|
+
return (
|
|
251
|
+
"Timed out reaching that URL — DNS may not point to this server yet, or HTTPS is not ready. "
|
|
252
|
+
"Add the A record, run Set up HTTPS, wait a minute, then retry."
|
|
253
|
+
)
|
|
254
|
+
if local.get("local_ok"):
|
|
255
|
+
return "Workframe is running on this server; the public URL is not reachable from here yet."
|
|
256
|
+
return "Check DNS, HTTPS (Caddy), and that the domain matches this server."
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def url_test(app_base_url: str) -> dict[str, Any]:
|
|
260
|
+
url = _normalize_app_base_url(app_base_url)
|
|
261
|
+
if not url:
|
|
262
|
+
raise ValueError("app_base_url required")
|
|
263
|
+
host = _hostname_only(url)
|
|
264
|
+
|
|
265
|
+
if _loopback_hostname(host):
|
|
266
|
+
try:
|
|
267
|
+
local = _local_stack_health()
|
|
268
|
+
if local.get("local_ok"):
|
|
269
|
+
return {
|
|
270
|
+
"ok": True,
|
|
271
|
+
"status": local.get("status"),
|
|
272
|
+
"url": url,
|
|
273
|
+
"checked_url": local.get("checked_url"),
|
|
274
|
+
"hint": "Loopback URL — verified via the UI proxy on this stack.",
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
"ok": False,
|
|
278
|
+
"url": url,
|
|
279
|
+
"error": str(local.get("error") or "local health check failed"),
|
|
280
|
+
"hint": "Is the Workframe UI container running?",
|
|
281
|
+
}
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
return {
|
|
284
|
+
"ok": False,
|
|
285
|
+
"url": url,
|
|
286
|
+
"error": str(exc),
|
|
287
|
+
"hint": "Is the Workframe UI container running?",
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
health_url = f"{url.rstrip('/')}/api/health"
|
|
291
|
+
local = _local_stack_health()
|
|
292
|
+
try:
|
|
293
|
+
return {**_fetch_health(health_url), "url": health_url}
|
|
294
|
+
except urllib.error.HTTPError as exc:
|
|
295
|
+
return {
|
|
296
|
+
"ok": False,
|
|
297
|
+
"status": exc.code,
|
|
298
|
+
"url": health_url,
|
|
299
|
+
"error": str(exc),
|
|
300
|
+
"hint": _url_test_hint(exc, local=local),
|
|
301
|
+
"local_ok": bool(local.get("local_ok")),
|
|
302
|
+
}
|
|
303
|
+
except Exception as exc:
|
|
304
|
+
return {
|
|
305
|
+
"ok": False,
|
|
306
|
+
"url": health_url,
|
|
307
|
+
"error": str(exc),
|
|
308
|
+
"hint": _url_test_hint(exc, local=local),
|
|
309
|
+
"local_ok": bool(local.get("local_ok")),
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def smtp_error_hint(exc: Exception) -> str:
|
|
314
|
+
msg = str(exc).lower()
|
|
315
|
+
if "smtp login failed" in msg or "535" in msg or "badcredentials" in msg:
|
|
316
|
+
return "Check SMTP username and app password. For Gmail, use an App Password—not your normal password."
|
|
317
|
+
if "smtp password is required" in msg:
|
|
318
|
+
return "Enter the SMTP app password. Compose uses SMTP_PASS; onboarding saves it in stack config."
|
|
319
|
+
if "rejected from address" in msg:
|
|
320
|
+
return (
|
|
321
|
+
"Gmail rejected that From address for this login. Leave From blank to send as your login email, "
|
|
322
|
+
"or add the address under Gmail Settings → Accounts → Send mail as."
|
|
323
|
+
)
|
|
324
|
+
if "530" in msg and "authentication required" in msg:
|
|
325
|
+
return "SMTP authentication did not complete. Check username, app password, and port (465=SSL, 587=STARTTLS)."
|
|
326
|
+
if "connect" in msg or "timed out" in msg or "unexpectedly closed" in msg:
|
|
327
|
+
return "Check SMTP host and port. Port 465 needs SSL; port 587 uses STARTTLS."
|
|
328
|
+
if "certificate" in msg or "ssl" in msg:
|
|
329
|
+
return "Try toggling TLS/SSL settings to match your provider."
|
|
330
|
+
return "Double-check host, port, username, password, and From address, then try again."
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def normalize_setup_https(host: str, port: int | str | None = None) -> tuple[str, int]:
|
|
334
|
+
name = _hostname_only(host) or str(host or "").strip().lower()
|
|
335
|
+
if not name or not re.fullmatch(r"[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?", name, re.IGNORECASE):
|
|
336
|
+
raise ValueError("valid hostname required")
|
|
337
|
+
ui_port = int(port or os.environ.get("WORKFRAME_UI_PORT", "18644") or "18644")
|
|
338
|
+
return name, ui_port
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
assert dns_record_name("dev.alanborger.com") == "dev"
|
|
343
|
+
assert dns_record_name("alanborger.com") == "@"
|
|
344
|
+
assert apex_domain("dev.alanborger.com") == "alanborger.com"
|
|
345
|
+
assert _loopback_hostname("127.0.0.1")
|
|
346
|
+
assert _loopback_hostname("localhost")
|
|
347
|
+
assert not _loopback_hostname("dev.example.com")
|
|
348
|
+
import tempfile
|
|
349
|
+
from pathlib import Path
|
|
350
|
+
|
|
351
|
+
td = Path(tempfile.mkdtemp())
|
|
352
|
+
os.environ["WORKFRAME_API_DATA_DIR"] = str(td)
|
|
353
|
+
stack_config.patch_stack_config({"smtp": {"host": "smtp.test", "user": "a", "password": "b"}})
|
|
354
|
+
assert not stack_config.smtp_tested()
|
|
355
|
+
stack_config.mark_smtp_tested()
|
|
356
|
+
assert stack_config.smtp_tested()
|
|
357
|
+
stack_config.patch_stack_config({"smtp": {"host": "smtp.other"}})
|
|
358
|
+
assert not stack_config.smtp_tested()
|
|
359
|
+
print("install_api publish hints ok")
|