create-workframe 0.1.0 → 0.1.1

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.
Files changed (206) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +12 -12
  3. package/README.md +8 -92
  4. package/SECURITY.md +40 -40
  5. package/bin/workframe.js +329 -329
  6. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
  7. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
  8. package/package.json +3 -6
  9. package/profiles/architect/AGENTS.md +29 -29
  10. package/profiles/architect/SOUL.md +2 -2
  11. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
  12. package/profiles/designer/AGENTS.md +26 -26
  13. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
  14. package/profiles/dev/AGENTS.md +28 -28
  15. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
  16. package/profiles/docs/AGENTS.md +27 -27
  17. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
  18. package/profiles/research/AGENTS.md +26 -26
  19. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
  20. package/profiles/visionary/AGENTS.md +25 -25
  21. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
  22. package/profiles/workframe-agent/AGENTS.md +37 -37
  23. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
  24. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
  25. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
  26. package/rules/workspace-README.md +5 -5
  27. package/scripts/apply-update-hermes.sh +17 -17
  28. package/scripts/apply-update-workframe.sh +77 -77
  29. package/scripts/bootstrap-workspace-link.sh +8 -8
  30. package/scripts/bundle-workframe-ui.mjs +3 -3
  31. package/scripts/compose-docker-host.sh +37 -37
  32. package/scripts/ensure-compose-host-paths.mjs +51 -51
  33. package/scripts/fix-zk-encryption-key.sh +35 -35
  34. package/scripts/lib/install-identity.mjs +212 -212
  35. package/scripts/restart-gateway-hermes.sh +12 -12
  36. package/scripts/set-compose-public-url.mjs +92 -92
  37. package/scripts/setup-stack-secrets.sh +50 -50
  38. package/scripts/sync-canonical-to-package.mjs +8 -7
  39. package/scripts/verify-public-deploy.sh +105 -105
  40. package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
  41. package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
  42. package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
  43. package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
  44. package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
  45. package/workframe-api/README.md +28 -28
  46. package/workframe-api/action_proxy.py +131 -131
  47. package/workframe-api/auth_rate_limit.py +49 -49
  48. package/workframe-api/credential_vault.py +445 -445
  49. package/workframe-api/data/avatar-catalog.json +41 -41
  50. package/workframe-api/email_sender.py +220 -220
  51. package/workframe-api/google_auth.py +90 -90
  52. package/workframe-api/install_api.py +359 -359
  53. package/workframe-api/internal_proxy_auth.py +150 -150
  54. package/workframe-api/llm_proxy.py +277 -277
  55. package/workframe-api/oidc_jwt.py +108 -108
  56. package/workframe-api/package.json +12 -13
  57. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  58. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  59. package/workframe-api/requirements.txt +2 -2
  60. package/workframe-api/site_meta.py +271 -271
  61. package/workframe-api/stack_config.py +427 -427
  62. package/workframe-api/time-bind-chat.py +99 -99
  63. package/workframe-api/turn_credentials.py +226 -226
  64. package/workframe-api/updates.py +417 -417
  65. package/workframe-api/vault_kek.py +159 -159
  66. package/workframe-api/zk_auth.py +633 -633
  67. package/workframe-supervisor/Dockerfile +11 -11
  68. package/workframe-supervisor/server.py +787 -787
  69. package/workframe-ui/docker/nginx.conf +85 -85
  70. package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
  71. package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
  72. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
  73. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
  74. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
  75. package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
  76. package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
  77. package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
  78. package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
  79. package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
  80. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
  81. package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
  82. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
  83. package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
  84. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
  85. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
  86. package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
  87. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
  88. package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
  89. package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
  90. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
  91. package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
  92. package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
  93. package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
  94. package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
  95. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
  96. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
  97. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
  98. package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
  99. package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
  100. package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
  101. package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
  102. package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
  103. package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
  104. package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
  105. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
  106. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
  107. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
  108. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
  109. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
  110. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
  111. package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
  112. package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
  113. package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
  114. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
  115. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
  116. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
  117. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
  118. package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
  119. package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
  120. package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
  121. package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
  122. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
  123. package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
  124. package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
  125. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
  126. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
  127. package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
  128. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
  129. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
  130. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
  131. package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
  132. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
  133. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
  134. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
  135. package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
  136. package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
  137. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
  138. package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
  139. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
  140. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
  141. package/workframe-ui/public/favicon.svg +7 -7
  142. package/workframe-ui/public/index.html +50 -50
  143. package/workframe-ui/public/workframe-config.json +3 -3
  144. package/scripts/security_audit.py +0 -156
  145. package/scripts/test-scaffold.mjs +0 -390
  146. package/workframe-api/tests/__init__.py +0 -0
  147. package/workframe-api/tests/db_setup.py +0 -13
  148. package/workframe-api/tests/test_admin_updates_gated.py +0 -30
  149. package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
  150. package/workframe-api/tests/test_agent_profile_sync.py +0 -76
  151. package/workframe-api/tests/test_auth_email.py +0 -222
  152. package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
  153. package/workframe-api/tests/test_auth_rate_limit.py +0 -19
  154. package/workframe-api/tests/test_avatar_resolve.py +0 -77
  155. package/workframe-api/tests/test_child_soul_template.py +0 -71
  156. package/workframe-api/tests/test_credential_canary.py +0 -135
  157. package/workframe-api/tests/test_credential_isolation.py +0 -448
  158. package/workframe-api/tests/test_credential_resolution.py +0 -206
  159. package/workframe-api/tests/test_device_oauth.py +0 -108
  160. package/workframe-api/tests/test_doctor_repair.py +0 -103
  161. package/workframe-api/tests/test_ensure_profile_api.py +0 -77
  162. package/workframe-api/tests/test_gateway_compose_security.py +0 -136
  163. package/workframe-api/tests/test_install_secure_host.py +0 -39
  164. package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
  165. package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
  166. package/workframe-api/tests/test_kanban_delegation.py +0 -185
  167. package/workframe-api/tests/test_llm_proxy.py +0 -155
  168. package/workframe-api/tests/test_login_access_policy.py +0 -183
  169. package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
  170. package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
  171. package/workframe-api/tests/test_platform_auth.py +0 -47
  172. package/workframe-api/tests/test_profile_config_path.py +0 -56
  173. package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
  174. package/workframe-api/tests/test_profile_create.py +0 -72
  175. package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
  176. package/workframe-api/tests/test_profile_install_health.py +0 -45
  177. package/workframe-api/tests/test_profile_secret_policy.py +0 -57
  178. package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
  179. package/workframe-api/tests/test_provider_bootstrap.py +0 -75
  180. package/workframe-api/tests/test_provider_connect.py +0 -54
  181. package/workframe-api/tests/test_room_crud.py +0 -192
  182. package/workframe-api/tests/test_room_tenancy.py +0 -701
  183. package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
  184. package/workframe-api/tests/test_site_meta.py +0 -81
  185. package/workframe-api/tests/test_soul_stub.py +0 -42
  186. package/workframe-api/tests/test_space_member_sync.py +0 -99
  187. package/workframe-api/tests/test_stripe_stack_config.py +0 -37
  188. package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
  189. package/workframe-api/tests/test_turn_credential_vault.py +0 -125
  190. package/workframe-api/tests/test_updates.py +0 -176
  191. package/workframe-api/tests/test_user_cohort.py +0 -113
  192. package/workframe-api/tests/test_vault_envelope.py +0 -110
  193. package/workframe-api/tests/test_workspace_members.py +0 -183
  194. package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
  195. package/workframe-api/tests/test_workspace_provider_list.py +0 -57
  196. package/workframe-supervisor/tests/test_exec_guard.py +0 -42
  197. package/workframe-supervisor/tests/test_server_import.py +0 -21
  198. package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
  199. package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
  200. package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
  201. package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
  202. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
  203. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
  204. package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
  205. package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
  206. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +0 -1
@@ -1,41 +1,41 @@
1
- {
2
- "version": 2,
3
- "public_base": "/assets/agents",
4
- "avatars": [
5
- { "id": "ada", "file": "ada.png", "label": "Ada" },
6
- { "id": "aibert", "file": "aibert.png", "label": "Aibert" },
7
- { "id": "amelia", "file": "amelia.png", "label": "Amelia" },
8
- { "id": "andy", "file": "andy.png", "label": "Andy" },
9
- { "id": "bob", "file": "bob.png", "label": "Bob" },
10
- { "id": "buzz", "file": "buzz.png", "label": "Buzz" },
11
- { "id": "carl", "file": "carl.png", "label": "Carl" },
12
- { "id": "corbu", "file": "corbu.png", "label": "Corbu" },
13
- { "id": "diana", "file": "diana.png", "label": "Diana" },
14
- { "id": "ella", "file": "ella.png", "label": "Ella" },
15
- { "id": "elvis", "file": "elvis.png", "label": "Elvis" },
16
- { "id": "frida", "file": "frida.png", "label": "Frida" },
17
- { "id": "george", "file": "george.png", "label": "George" },
18
- { "id": "grace", "file": "grace.png", "label": "Grace" },
19
- { "id": "hedy", "file": "hedy.png", "label": "Hedy" },
20
- { "id": "isaac", "file": "isaac.png", "label": "Isaac" },
21
- { "id": "john", "file": "john.png", "label": "John" },
22
- { "id": "joni", "file": "joni.png", "label": "Joni" },
23
- { "id": "leo", "file": "leo.png", "label": "Leo" },
24
- { "id": "louis", "file": "louis.png", "label": "Louis" },
25
- { "id": "ludwig", "file": "ludwig.png", "label": "Ludwig" },
26
- { "id": "marie", "file": "marie.png", "label": "Marie" },
27
- { "id": "marilyn", "file": "marilyn.png", "label": "Marilyn" },
28
- { "id": "neil", "file": "neil.png", "label": "Neil" },
29
- { "id": "nikola", "file": "nikola.png", "label": "Nikola" },
30
- { "id": "nina", "file": "nina.png", "label": "Nina" },
31
- { "id": "paul", "file": "paul.png", "label": "Paul" },
32
- { "id": "ringo", "file": "ringo.png", "label": "Ringo" },
33
- { "id": "rosie", "file": "rosie.png", "label": "Rosie" },
34
- { "id": "steve", "file": "steve.png", "label": "Steve" },
35
- { "id": "sun", "file": "sun.png", "label": "Sun" },
36
- { "id": "tom", "file": "tom.png", "label": "Tom" },
37
- { "id": "warren", "file": "warren.png", "label": "Warren" },
38
- { "id": "woz", "file": "woz.png", "label": "Woz" },
39
- { "id": "zaha", "file": "zaha.png", "label": "Zaha" }
40
- ]
41
- }
1
+ {
2
+ "version": 2,
3
+ "public_base": "/assets/agents",
4
+ "avatars": [
5
+ { "id": "ada", "file": "ada.png", "label": "Ada" },
6
+ { "id": "aibert", "file": "aibert.png", "label": "Aibert" },
7
+ { "id": "amelia", "file": "amelia.png", "label": "Amelia" },
8
+ { "id": "andy", "file": "andy.png", "label": "Andy" },
9
+ { "id": "bob", "file": "bob.png", "label": "Bob" },
10
+ { "id": "buzz", "file": "buzz.png", "label": "Buzz" },
11
+ { "id": "carl", "file": "carl.png", "label": "Carl" },
12
+ { "id": "corbu", "file": "corbu.png", "label": "Corbu" },
13
+ { "id": "diana", "file": "diana.png", "label": "Diana" },
14
+ { "id": "ella", "file": "ella.png", "label": "Ella" },
15
+ { "id": "elvis", "file": "elvis.png", "label": "Elvis" },
16
+ { "id": "frida", "file": "frida.png", "label": "Frida" },
17
+ { "id": "george", "file": "george.png", "label": "George" },
18
+ { "id": "grace", "file": "grace.png", "label": "Grace" },
19
+ { "id": "hedy", "file": "hedy.png", "label": "Hedy" },
20
+ { "id": "isaac", "file": "isaac.png", "label": "Isaac" },
21
+ { "id": "john", "file": "john.png", "label": "John" },
22
+ { "id": "joni", "file": "joni.png", "label": "Joni" },
23
+ { "id": "leo", "file": "leo.png", "label": "Leo" },
24
+ { "id": "louis", "file": "louis.png", "label": "Louis" },
25
+ { "id": "ludwig", "file": "ludwig.png", "label": "Ludwig" },
26
+ { "id": "marie", "file": "marie.png", "label": "Marie" },
27
+ { "id": "marilyn", "file": "marilyn.png", "label": "Marilyn" },
28
+ { "id": "neil", "file": "neil.png", "label": "Neil" },
29
+ { "id": "nikola", "file": "nikola.png", "label": "Nikola" },
30
+ { "id": "nina", "file": "nina.png", "label": "Nina" },
31
+ { "id": "paul", "file": "paul.png", "label": "Paul" },
32
+ { "id": "ringo", "file": "ringo.png", "label": "Ringo" },
33
+ { "id": "rosie", "file": "rosie.png", "label": "Rosie" },
34
+ { "id": "steve", "file": "steve.png", "label": "Steve" },
35
+ { "id": "sun", "file": "sun.png", "label": "Sun" },
36
+ { "id": "tom", "file": "tom.png", "label": "Tom" },
37
+ { "id": "warren", "file": "warren.png", "label": "Warren" },
38
+ { "id": "woz", "file": "woz.png", "label": "Woz" },
39
+ { "id": "zaha", "file": "zaha.png", "label": "Zaha" }
40
+ ]
41
+ }
@@ -1,220 +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
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