create-workframe 0.1.0 → 0.1.2

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