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.
Files changed (415) hide show
  1. package/.dockerignore +22 -0
  2. package/.gitignore +73 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +12 -0
  5. package/README.md +111 -0
  6. package/SECURITY.md +40 -0
  7. package/bin/create-workframe.js +2814 -0
  8. package/bin/workframe.js +329 -0
  9. package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -0
  10. package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -0
  11. package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -0
  12. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -0
  13. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -0
  14. package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -0
  15. package/package.json +67 -0
  16. package/profiles/README.md +15 -0
  17. package/profiles/architect/AGENTS.md +29 -0
  18. package/profiles/architect/SOUL.md +44 -0
  19. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -0
  20. package/profiles/designer/AGENTS.md +26 -0
  21. package/profiles/designer/SOUL.md +31 -0
  22. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -0
  23. package/profiles/dev/AGENTS.md +28 -0
  24. package/profiles/dev/SOUL.md +31 -0
  25. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -0
  26. package/profiles/docs/AGENTS.md +27 -0
  27. package/profiles/docs/SOUL.md +30 -0
  28. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -0
  29. package/profiles/research/AGENTS.md +26 -0
  30. package/profiles/research/SOUL.md +31 -0
  31. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -0
  32. package/profiles/visionary/AGENTS.md +25 -0
  33. package/profiles/visionary/SOUL.md +31 -0
  34. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -0
  35. package/profiles/workframe-agent/AGENTS.md +37 -0
  36. package/profiles/workframe-agent/SETUP.md +185 -0
  37. package/profiles/workframe-agent/SOUL.md +61 -0
  38. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -0
  39. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -0
  40. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -0
  41. package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -0
  42. package/rules/.hermes.md +11 -0
  43. package/rules/AGENTS.md +22 -0
  44. package/rules/workspace-README.md +5 -0
  45. package/scripts/apply-update-hermes.sh +17 -0
  46. package/scripts/apply-update-workframe.sh +77 -0
  47. package/scripts/bootstrap-workspace-link.sh +8 -0
  48. package/scripts/bundle-workframe-ui.mjs +77 -0
  49. package/scripts/compose-docker-host.sh +37 -0
  50. package/scripts/create_workframe_scaffold.py +648 -0
  51. package/scripts/ensure-compose-host-paths.mjs +51 -0
  52. package/scripts/fix-zk-encryption-key.sh +35 -0
  53. package/scripts/lib/install-identity.mjs +212 -0
  54. package/scripts/lib/workframe-registry.mjs +290 -0
  55. package/scripts/new-project.mjs +68 -0
  56. package/scripts/restart-gateway-hermes.sh +12 -0
  57. package/scripts/security_audit.py +156 -0
  58. package/scripts/select_agent_pack.py +31 -0
  59. package/scripts/set-compose-public-url.mjs +92 -0
  60. package/scripts/setup-stack-secrets.sh +50 -0
  61. package/scripts/sync-canonical-to-package.mjs +146 -0
  62. package/scripts/test-scaffold.mjs +390 -0
  63. package/scripts/verify-public-deploy.sh +105 -0
  64. package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -0
  65. package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -0
  66. package/shared/WORKFRAME_AGENT_PACKS.json +64 -0
  67. package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -0
  68. package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -0
  69. package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -0
  70. package/shared/WORKFRAME_SKILL_CURATION.md +27 -0
  71. package/shared/agent-avatars/ada.png +0 -0
  72. package/shared/agent-avatars/aibert.png +0 -0
  73. package/shared/agent-avatars/amelia.png +0 -0
  74. package/shared/agent-avatars/andy.png +0 -0
  75. package/shared/agent-avatars/arc.png +0 -0
  76. package/shared/agent-avatars/bob.png +0 -0
  77. package/shared/agent-avatars/buzz.png +0 -0
  78. package/shared/agent-avatars/carl.png +0 -0
  79. package/shared/agent-avatars/catalog.json +171 -0
  80. package/shared/agent-avatars/corbu.png +0 -0
  81. package/shared/agent-avatars/diana.png +0 -0
  82. package/shared/agent-avatars/ella.png +0 -0
  83. package/shared/agent-avatars/elvis.png +0 -0
  84. package/shared/agent-avatars/f1.png +0 -0
  85. package/shared/agent-avatars/f2.png +0 -0
  86. package/shared/agent-avatars/f3.png +0 -0
  87. package/shared/agent-avatars/f4.png +0 -0
  88. package/shared/agent-avatars/f5.png +0 -0
  89. package/shared/agent-avatars/f6.png +0 -0
  90. package/shared/agent-avatars/frida.png +0 -0
  91. package/shared/agent-avatars/george.png +0 -0
  92. package/shared/agent-avatars/grace.png +0 -0
  93. package/shared/agent-avatars/hedy.png +0 -0
  94. package/shared/agent-avatars/hermes.png +0 -0
  95. package/shared/agent-avatars/isaac.png +0 -0
  96. package/shared/agent-avatars/jes.png +0 -0
  97. package/shared/agent-avatars/john.png +0 -0
  98. package/shared/agent-avatars/joni.png +0 -0
  99. package/shared/agent-avatars/leo.png +0 -0
  100. package/shared/agent-avatars/louis.png +0 -0
  101. package/shared/agent-avatars/ludwig.png +0 -0
  102. package/shared/agent-avatars/m1.png +0 -0
  103. package/shared/agent-avatars/m2.png +0 -0
  104. package/shared/agent-avatars/m3.png +0 -0
  105. package/shared/agent-avatars/m4.png +0 -0
  106. package/shared/agent-avatars/m5.png +0 -0
  107. package/shared/agent-avatars/m6.png +0 -0
  108. package/shared/agent-avatars/marie.png +0 -0
  109. package/shared/agent-avatars/marilyn.png +0 -0
  110. package/shared/agent-avatars/neil.png +0 -0
  111. package/shared/agent-avatars/nikola.png +0 -0
  112. package/shared/agent-avatars/nina.png +0 -0
  113. package/shared/agent-avatars/paul.png +0 -0
  114. package/shared/agent-avatars/ringo.png +0 -0
  115. package/shared/agent-avatars/rosie.png +0 -0
  116. package/shared/agent-avatars/ste.png +0 -0
  117. package/shared/agent-avatars/steve.png +0 -0
  118. package/shared/agent-avatars/sun.png +0 -0
  119. package/shared/agent-avatars/tom.png +0 -0
  120. package/shared/agent-avatars/warren.png +0 -0
  121. package/shared/agent-avatars/woz.png +0 -0
  122. package/shared/agent-avatars/zaha.png +0 -0
  123. package/workframe-api/Dockerfile +14 -0
  124. package/workframe-api/README.md +28 -0
  125. package/workframe-api/action_proxy.py +131 -0
  126. package/workframe-api/auth_rate_limit.py +49 -0
  127. package/workframe-api/catalog/avatar-catalog.json +171 -0
  128. package/workframe-api/catalog/logo-catalog.json +86 -0
  129. package/workframe-api/catalog/user-avatar-catalog.json +171 -0
  130. package/workframe-api/credential_vault.py +445 -0
  131. package/workframe-api/data/.gitkeep +0 -0
  132. package/workframe-api/data/avatar-catalog.json +41 -0
  133. package/workframe-api/data/logo-catalog.json +14 -0
  134. package/workframe-api/data/user-avatar-catalog.json +18 -0
  135. package/workframe-api/email_sender.py +220 -0
  136. package/workframe-api/google_auth.py +90 -0
  137. package/workframe-api/install_api.py +359 -0
  138. package/workframe-api/internal_proxy_auth.py +150 -0
  139. package/workframe-api/llm_proxy.py +277 -0
  140. package/workframe-api/oidc_jwt.py +108 -0
  141. package/workframe-api/package.json +13 -0
  142. package/workframe-api/platform_auth.py +194 -0
  143. package/workframe-api/profile_secret_policy.py +86 -0
  144. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -0
  145. package/workframe-api/public/assets/index-DYnLrCZZ.js +9 -0
  146. package/workframe-api/public/assets/index-DglUqFB_.js +9 -0
  147. package/workframe-api/public/index.html +12 -0
  148. package/workframe-api/requirements.txt +2 -0
  149. package/workframe-api/server.py +19646 -0
  150. package/workframe-api/site_meta.py +271 -0
  151. package/workframe-api/stack_config.py +427 -0
  152. package/workframe-api/tests/__init__.py +0 -0
  153. package/workframe-api/tests/db_setup.py +13 -0
  154. package/workframe-api/tests/test_admin_updates_gated.py +30 -0
  155. package/workframe-api/tests/test_agent_dm_bootstrap.py +196 -0
  156. package/workframe-api/tests/test_agent_profile_sync.py +76 -0
  157. package/workframe-api/tests/test_auth_email.py +222 -0
  158. package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +99 -0
  159. package/workframe-api/tests/test_auth_rate_limit.py +19 -0
  160. package/workframe-api/tests/test_avatar_resolve.py +77 -0
  161. package/workframe-api/tests/test_child_soul_template.py +71 -0
  162. package/workframe-api/tests/test_credential_canary.py +135 -0
  163. package/workframe-api/tests/test_credential_isolation.py +448 -0
  164. package/workframe-api/tests/test_credential_resolution.py +206 -0
  165. package/workframe-api/tests/test_device_oauth.py +108 -0
  166. package/workframe-api/tests/test_doctor_repair.py +103 -0
  167. package/workframe-api/tests/test_ensure_profile_api.py +77 -0
  168. package/workframe-api/tests/test_gateway_compose_security.py +136 -0
  169. package/workframe-api/tests/test_install_secure_host.py +39 -0
  170. package/workframe-api/tests/test_internal_proxy_auth.py +125 -0
  171. package/workframe-api/tests/test_invite_runtime_bootstrap.py +72 -0
  172. package/workframe-api/tests/test_kanban_delegation.py +185 -0
  173. package/workframe-api/tests/test_llm_proxy.py +155 -0
  174. package/workframe-api/tests/test_login_access_policy.py +183 -0
  175. package/workframe-api/tests/test_mvp_model_bootstrap.py +75 -0
  176. package/workframe-api/tests/test_onboarding_bootstrap.py +248 -0
  177. package/workframe-api/tests/test_platform_auth.py +47 -0
  178. package/workframe-api/tests/test_profile_config_path.py +56 -0
  179. package/workframe-api/tests/test_profile_config_yaml_repair.py +63 -0
  180. package/workframe-api/tests/test_profile_create.py +72 -0
  181. package/workframe-api/tests/test_profile_identity_overlay.py +61 -0
  182. package/workframe-api/tests/test_profile_install_health.py +45 -0
  183. package/workframe-api/tests/test_profile_secret_policy.py +57 -0
  184. package/workframe-api/tests/test_profile_workspace_cwd.py +34 -0
  185. package/workframe-api/tests/test_provider_bootstrap.py +75 -0
  186. package/workframe-api/tests/test_provider_connect.py +54 -0
  187. package/workframe-api/tests/test_room_crud.py +192 -0
  188. package/workframe-api/tests/test_room_tenancy.py +701 -0
  189. package/workframe-api/tests/test_runtime_identity_backfill.py +34 -0
  190. package/workframe-api/tests/test_site_meta.py +81 -0
  191. package/workframe-api/tests/test_soul_stub.py +42 -0
  192. package/workframe-api/tests/test_space_member_sync.py +99 -0
  193. package/workframe-api/tests/test_stripe_stack_config.py +37 -0
  194. package/workframe-api/tests/test_supervisor_lifecycle.py +52 -0
  195. package/workframe-api/tests/test_turn_credential_vault.py +125 -0
  196. package/workframe-api/tests/test_updates.py +176 -0
  197. package/workframe-api/tests/test_user_cohort.py +113 -0
  198. package/workframe-api/tests/test_vault_envelope.py +110 -0
  199. package/workframe-api/tests/test_workspace_members.py +183 -0
  200. package/workframe-api/tests/test_workspace_messaging_sync.py +125 -0
  201. package/workframe-api/tests/test_workspace_provider_list.py +57 -0
  202. package/workframe-api/time-bind-chat.py +99 -0
  203. package/workframe-api/turn_credentials.py +226 -0
  204. package/workframe-api/updates.py +417 -0
  205. package/workframe-api/vault_kek.py +159 -0
  206. package/workframe-api/zk_auth.py +633 -0
  207. package/workframe-supervisor/Dockerfile +11 -0
  208. package/workframe-supervisor/profile_secret_policy.py +76 -0
  209. package/workframe-supervisor/server.py +787 -0
  210. package/workframe-supervisor/tests/test_exec_guard.py +42 -0
  211. package/workframe-supervisor/tests/test_server_import.py +21 -0
  212. package/workframe-ui/docker/nginx.conf +85 -0
  213. package/workframe-ui/public/assets/1-DLJbBkOb.png +0 -0
  214. package/workframe-ui/public/assets/10-uwRwj5ce.png +0 -0
  215. package/workframe-ui/public/assets/11-5OuV9F_e.png +0 -0
  216. package/workframe-ui/public/assets/12-u_axjxW-.png +0 -0
  217. package/workframe-ui/public/assets/13-ldSvcMsH.png +0 -0
  218. package/workframe-ui/public/assets/14-xdcALEYD.png +0 -0
  219. package/workframe-ui/public/assets/15-aZ4snEFB.png +0 -0
  220. package/workframe-ui/public/assets/16-L_5-DttY.png +0 -0
  221. package/workframe-ui/public/assets/2-zOPZTppD.png +0 -0
  222. package/workframe-ui/public/assets/3-Dc3WoVu5.png +0 -0
  223. package/workframe-ui/public/assets/4-C50hk7_m.png +0 -0
  224. package/workframe-ui/public/assets/5-Eweetkq4.png +0 -0
  225. package/workframe-ui/public/assets/6-5sOXgfkw.png +0 -0
  226. package/workframe-ui/public/assets/7-BqRBCbiC.png +0 -0
  227. package/workframe-ui/public/assets/8-DEDKS94h.png +0 -0
  228. package/workframe-ui/public/assets/9-DNj34GW-.png +0 -0
  229. package/workframe-ui/public/assets/ada-DsvuOc9n.png +0 -0
  230. package/workframe-ui/public/assets/aibert-BCz8Lo8H.png +0 -0
  231. package/workframe-ui/public/assets/amelia-DUf3EBGu.png +0 -0
  232. package/workframe-ui/public/assets/andy-Cpymuhhx.png +0 -0
  233. package/workframe-ui/public/assets/arc-CBDYvkAF.js +1 -0
  234. package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +1 -0
  235. package/workframe-ui/public/assets/architectureDiagram-3BPJPVTR-XnBRKeW0.js +36 -0
  236. package/workframe-ui/public/assets/array-BifhSqXX.js +1 -0
  237. package/workframe-ui/public/assets/avatars/ada.png +0 -0
  238. package/workframe-ui/public/assets/avatars/aibert.png +0 -0
  239. package/workframe-ui/public/assets/avatars/amelia.png +0 -0
  240. package/workframe-ui/public/assets/avatars/andy.png +0 -0
  241. package/workframe-ui/public/assets/avatars/bob.png +0 -0
  242. package/workframe-ui/public/assets/avatars/buzz.png +0 -0
  243. package/workframe-ui/public/assets/avatars/carl.png +0 -0
  244. package/workframe-ui/public/assets/avatars/catalog.json +171 -0
  245. package/workframe-ui/public/assets/avatars/corbu.png +0 -0
  246. package/workframe-ui/public/assets/avatars/diana.png +0 -0
  247. package/workframe-ui/public/assets/avatars/elvis.png +0 -0
  248. package/workframe-ui/public/assets/avatars/frida.png +0 -0
  249. package/workframe-ui/public/assets/avatars/george.png +0 -0
  250. package/workframe-ui/public/assets/avatars/grace.png +0 -0
  251. package/workframe-ui/public/assets/avatars/hedy.png +0 -0
  252. package/workframe-ui/public/assets/avatars/hermes.png +0 -0
  253. package/workframe-ui/public/assets/avatars/isaac.png +0 -0
  254. package/workframe-ui/public/assets/avatars/john.png +0 -0
  255. package/workframe-ui/public/assets/avatars/joni.png +0 -0
  256. package/workframe-ui/public/assets/avatars/leo.png +0 -0
  257. package/workframe-ui/public/assets/avatars/louis.png +0 -0
  258. package/workframe-ui/public/assets/avatars/ludwig.png +0 -0
  259. package/workframe-ui/public/assets/avatars/marie.png +0 -0
  260. package/workframe-ui/public/assets/avatars/marilyn.png +0 -0
  261. package/workframe-ui/public/assets/avatars/nikola.png +0 -0
  262. package/workframe-ui/public/assets/avatars/nina.png +0 -0
  263. package/workframe-ui/public/assets/avatars/paul.png +0 -0
  264. package/workframe-ui/public/assets/avatars/ringo.png +0 -0
  265. package/workframe-ui/public/assets/avatars/rosie.png +0 -0
  266. package/workframe-ui/public/assets/avatars/steve.png +0 -0
  267. package/workframe-ui/public/assets/avatars/sun.png +0 -0
  268. package/workframe-ui/public/assets/avatars/warren.png +0 -0
  269. package/workframe-ui/public/assets/avatars/woz.png +0 -0
  270. package/workframe-ui/public/assets/avatars/zaha.png +0 -0
  271. package/workframe-ui/public/assets/blockDiagram-GPEHLZMM-VYHUfVhd.js +132 -0
  272. package/workframe-ui/public/assets/bob-DRz-48Id.png +0 -0
  273. package/workframe-ui/public/assets/branding/banner.png +0 -0
  274. package/workframe-ui/public/assets/branding/og-default.png +0 -0
  275. package/workframe-ui/public/assets/branding/workframe'white.png +0 -0
  276. package/workframe-ui/public/assets/branding/workframe-1.png +0 -0
  277. package/workframe-ui/public/assets/branding/workframe-2.png +0 -0
  278. package/workframe-ui/public/assets/branding/workframe-3.png +0 -0
  279. package/workframe-ui/public/assets/branding/workframe-4.png +0 -0
  280. package/workframe-ui/public/assets/branding/workframe-5.png +0 -0
  281. package/workframe-ui/public/assets/branding/workframe-banner.png +0 -0
  282. package/workframe-ui/public/assets/branding/workframe-logo-horizontal-mini.png +0 -0
  283. package/workframe-ui/public/assets/branding/workframe-logo-horizontal-nano.png +0 -0
  284. package/workframe-ui/public/assets/branding/workframe-logo-horizontal.png +0 -0
  285. package/workframe-ui/public/assets/branding/workframe-logo-vertical-alt.png +0 -0
  286. package/workframe-ui/public/assets/branding/workframe-logo-vertical.png +0 -0
  287. package/workframe-ui/public/assets/branding/workframe.png +0 -0
  288. package/workframe-ui/public/assets/buzz-mC4PtMvC.png +0 -0
  289. package/workframe-ui/public/assets/c4Diagram-AAUBKEIU-BTjUcJpm.js +10 -0
  290. package/workframe-ui/public/assets/carl-CtE74db_.png +0 -0
  291. package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +1 -0
  292. package/workframe-ui/public/assets/chunk-2J33WTMH-w7uu7R-b.js +1 -0
  293. package/workframe-ui/public/assets/chunk-3OPIFGDE-Cb9LtnDX.js +62 -0
  294. package/workframe-ui/public/assets/chunk-4BX2VUAB-DiQ-qCwH.js +1 -0
  295. package/workframe-ui/public/assets/chunk-55IACEB6-C-mLFr7z.js +1 -0
  296. package/workframe-ui/public/assets/chunk-5ZQYHXKU-DOesfiCI.js +2 -0
  297. package/workframe-ui/public/assets/chunk-727SXJPM-BJ3oBZuz.js +206 -0
  298. package/workframe-ui/public/assets/chunk-AQP2D5EJ-CCA6xpGs.js +231 -0
  299. package/workframe-ui/public/assets/chunk-BSJP7CBP-a0cMNFb2.js +1 -0
  300. package/workframe-ui/public/assets/chunk-CSCIHK7Q-kuqN8EIY.js +122 -0
  301. package/workframe-ui/public/assets/chunk-FMBD7UC4-DyPgYHCg.js +15 -0
  302. package/workframe-ui/public/assets/chunk-KSCS5N6A-CdUuvR0V.js +10 -0
  303. package/workframe-ui/public/assets/chunk-L5ZTLDWV-Dq9NoWmK.js +1 -0
  304. package/workframe-ui/public/assets/chunk-LZXEDZCA-p74rddlO.js +2 -0
  305. package/workframe-ui/public/assets/chunk-ND2GUHAM-DBD2u1Gz.js +1 -0
  306. package/workframe-ui/public/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
  307. package/workframe-ui/public/assets/chunk-NZK2D7GU-BeIeYFnd.js +1 -0
  308. package/workframe-ui/public/assets/chunk-O5CBEL6O-ClHc56ib.js +70 -0
  309. package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +1 -0
  310. package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +1 -0
  311. package/workframe-ui/public/assets/chunk-XPW4576I-EFr8R_1p.js +32 -0
  312. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +1 -0
  313. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +1 -0
  314. package/workframe-ui/public/assets/corbu-KiaMXzXQ.png +0 -0
  315. package/workframe-ui/public/assets/cose-bilkent-S5V4N54A-C7aPBODd.js +1 -0
  316. package/workframe-ui/public/assets/cytoscape.esm-h6BdjjI9.js +321 -0
  317. package/workframe-ui/public/assets/dagre-BM42HDAG-BdU1Rv-H.js +4 -0
  318. package/workframe-ui/public/assets/dagre-Bx709z4p.js +1 -0
  319. package/workframe-ui/public/assets/defaultLocale-C8Fc0cco.js +1 -0
  320. package/workframe-ui/public/assets/diagram-2AECGRRQ-DWowSo85.js +43 -0
  321. package/workframe-ui/public/assets/diagram-5GNKFQAL-MnxBbceO.js +10 -0
  322. package/workframe-ui/public/assets/diagram-KO2AKTUF-DQaLRXFf.js +3 -0
  323. package/workframe-ui/public/assets/diagram-LMA3HP47-CQaBud9k.js +24 -0
  324. package/workframe-ui/public/assets/diagram-OG6HWLK6-D8bAXbY9.js +24 -0
  325. package/workframe-ui/public/assets/diana-DW0MsL38.png +0 -0
  326. package/workframe-ui/public/assets/dist-DGpTLHr_.js +1 -0
  327. package/workframe-ui/public/assets/elvis-LCFaZIcT.png +0 -0
  328. package/workframe-ui/public/assets/erDiagram-TEJ5UH35-1E-xSvBK.js +85 -0
  329. package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +1 -0
  330. package/workframe-ui/public/assets/flowDiagram-I6XJVG4X-CgOVD5hu.js +162 -0
  331. package/workframe-ui/public/assets/frida-CXFA0w3F.png +0 -0
  332. package/workframe-ui/public/assets/ganttDiagram-6RSMTGT7-JFYAIauo.js +292 -0
  333. package/workframe-ui/public/assets/george-DBSH2Sm2.png +0 -0
  334. package/workframe-ui/public/assets/gitGraph-WXDBUCRP-B9REenIl.js +1 -0
  335. package/workframe-ui/public/assets/gitGraphDiagram-PVQCEYII-BQ7NcMSn.js +106 -0
  336. package/workframe-ui/public/assets/grace-BhV0UPc0.png +0 -0
  337. package/workframe-ui/public/assets/graphlib-B8gBHxth.js +1 -0
  338. package/workframe-ui/public/assets/hedy-BR2IHift.png +0 -0
  339. package/workframe-ui/public/assets/hermes-CqCzcE0y.png +0 -0
  340. package/workframe-ui/public/assets/index-Dnw6vjqb.js +133 -0
  341. package/workframe-ui/public/assets/index-DpAGxump.css +1 -0
  342. package/workframe-ui/public/assets/info-J43DQDTF-CL6-eTjH.js +1 -0
  343. package/workframe-ui/public/assets/infoDiagram-5YYISTIA-LJTODW4W.js +2 -0
  344. package/workframe-ui/public/assets/init-D6jRqBbL.js +1 -0
  345. package/workframe-ui/public/assets/isaac-D1nhJAuv.png +0 -0
  346. package/workframe-ui/public/assets/ishikawaDiagram-YF4QCWOH-bchrQVuo.js +70 -0
  347. package/workframe-ui/public/assets/john-zSPWwNi4.png +0 -0
  348. package/workframe-ui/public/assets/joni-BFLoyfJP.png +0 -0
  349. package/workframe-ui/public/assets/journeyDiagram-JHISSGLW-DkrvYuxP.js +139 -0
  350. package/workframe-ui/public/assets/kanban-definition-UN3LZRKU-DFRbj0IG.js +89 -0
  351. package/workframe-ui/public/assets/katex-Vhh-h91d.js +257 -0
  352. package/workframe-ui/public/assets/leo-C_3IOL11.png +0 -0
  353. package/workframe-ui/public/assets/line-Vd48P7-O.js +1 -0
  354. package/workframe-ui/public/assets/linear-Ckizh2G7.js +1 -0
  355. package/workframe-ui/public/assets/louis-DEEECFSX.png +0 -0
  356. package/workframe-ui/public/assets/ludwig-_hoKhhyK.png +0 -0
  357. package/workframe-ui/public/assets/marie-DET6MsfO.png +0 -0
  358. package/workframe-ui/public/assets/marilyn-DTqwt8Yh.png +0 -0
  359. package/workframe-ui/public/assets/mermaid-parser.core-Bkimsnqj.js +4 -0
  360. package/workframe-ui/public/assets/mermaid.core-x0TvVuPo.js +9 -0
  361. package/workframe-ui/public/assets/mindmap-definition-RKZ34NQL-6ykAFPEz.js +96 -0
  362. package/workframe-ui/public/assets/nikola-B4PtHrJv.png +0 -0
  363. package/workframe-ui/public/assets/nina-BYbrOn0d.png +0 -0
  364. package/workframe-ui/public/assets/ordinal-hYBb2elL.js +1 -0
  365. package/workframe-ui/public/assets/packet-YPE3B663-Dw3xgMDt.js +1 -0
  366. package/workframe-ui/public/assets/path-BWPyau1x.js +1 -0
  367. package/workframe-ui/public/assets/paul-CGURYQIn.png +0 -0
  368. package/workframe-ui/public/assets/pie-LRSECV5Y-DATysawG.js +1 -0
  369. package/workframe-ui/public/assets/pieDiagram-4H26LBE5-SJKD1S0S.js +30 -0
  370. package/workframe-ui/public/assets/project-logos/1.png +0 -0
  371. package/workframe-ui/public/assets/project-logos/10.png +0 -0
  372. package/workframe-ui/public/assets/project-logos/11.png +0 -0
  373. package/workframe-ui/public/assets/project-logos/12.png +0 -0
  374. package/workframe-ui/public/assets/project-logos/13.png +0 -0
  375. package/workframe-ui/public/assets/project-logos/14.png +0 -0
  376. package/workframe-ui/public/assets/project-logos/15.png +0 -0
  377. package/workframe-ui/public/assets/project-logos/16.png +0 -0
  378. package/workframe-ui/public/assets/project-logos/2.png +0 -0
  379. package/workframe-ui/public/assets/project-logos/3.png +0 -0
  380. package/workframe-ui/public/assets/project-logos/4.png +0 -0
  381. package/workframe-ui/public/assets/project-logos/5.png +0 -0
  382. package/workframe-ui/public/assets/project-logos/6.png +0 -0
  383. package/workframe-ui/public/assets/project-logos/7.png +0 -0
  384. package/workframe-ui/public/assets/project-logos/8.png +0 -0
  385. package/workframe-ui/public/assets/project-logos/9.png +0 -0
  386. package/workframe-ui/public/assets/project-logos/catalog.json +86 -0
  387. package/workframe-ui/public/assets/quadrantDiagram-W4KKPZXB-BrYDZX8q.js +7 -0
  388. package/workframe-ui/public/assets/radar-GUYGQ44K-BmWYPCds.js +1 -0
  389. package/workframe-ui/public/assets/requirementDiagram-4Y6WPE33-DwL9Mc8e.js +84 -0
  390. package/workframe-ui/public/assets/ringo-WhfUNOyY.png +0 -0
  391. package/workframe-ui/public/assets/rosie-CAtcIf87.png +0 -0
  392. package/workframe-ui/public/assets/rough.esm-CSKSodPl.js +1 -0
  393. package/workframe-ui/public/assets/sankeyDiagram-5OEKKPKP-DYIFsL8h.js +40 -0
  394. package/workframe-ui/public/assets/sequenceDiagram-3UESZ5HK-0-FPkFk8.js +162 -0
  395. package/workframe-ui/public/assets/src-B_od6b6h.js +1 -0
  396. package/workframe-ui/public/assets/stateDiagram-AJRCARHV-BQCiBk6u.js +1 -0
  397. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +1 -0
  398. package/workframe-ui/public/assets/steve-CgXXJ9EZ.png +0 -0
  399. package/workframe-ui/public/assets/sun-BLNAhoZd.png +0 -0
  400. package/workframe-ui/public/assets/timeline-definition-PNZ67QCA-DS3tFcXj.js +120 -0
  401. package/workframe-ui/public/assets/treeView-BLDUP644-DSyUCKLY.js +1 -0
  402. package/workframe-ui/public/assets/treemap-LRROVOQU-CEZaNh5Y.js +1 -0
  403. package/workframe-ui/public/assets/vennDiagram-CIIHVFJN-CD-Vc9NF.js +34 -0
  404. package/workframe-ui/public/assets/wardley-L42UT6IY-Drq5w1Mc.js +1 -0
  405. package/workframe-ui/public/assets/wardleyDiagram-YWT4CUSO-DouXDJoF.js +78 -0
  406. package/workframe-ui/public/assets/warren-DIH7UKMY.png +0 -0
  407. package/workframe-ui/public/assets/woz-D2yleG-V.png +0 -0
  408. package/workframe-ui/public/assets/xychartDiagram-2RQKCTM6-DDf_Lol5.js +7 -0
  409. package/workframe-ui/public/assets/zaha-wersOEq9.png +0 -0
  410. package/workframe-ui/public/favicon.ico +0 -0
  411. package/workframe-ui/public/favicon.svg +7 -0
  412. package/workframe-ui/public/icons.svg +24 -0
  413. package/workframe-ui/public/index.html +50 -0
  414. package/workframe-ui/public/manifest.webmanifest +18 -0
  415. package/workframe-ui/public/workframe-config.json +4 -0
@@ -0,0 +1,150 @@
1
+ """Shared auth for /internal/llm and /internal/action — network + optional proxy token."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hmac
6
+ import ipaddress
7
+ import os
8
+ import secrets
9
+ from http.server import BaseHTTPRequestHandler
10
+ from pathlib import Path
11
+
12
+ PROXY_TOKEN_ENV = "WORKFRAME_PROXY_TOKEN"
13
+ PROXY_TOKEN_HEADER = "X-Workframe-Proxy-Token"
14
+ PROFILE_HEADER = "X-Workframe-Profile"
15
+ PROXY_TOKEN_FILE = ".proxy_token"
16
+ SHARED_PROXY_TOKEN_PATH = Path("/run/workframe-proxy/token")
17
+
18
+ _TOKEN: str | None = None
19
+ _TOKEN_PATH: Path | None = None
20
+
21
+
22
+ def _data_dir() -> Path:
23
+ return Path(
24
+ os.environ.get("WORKFRAME_API_DATA_DIR")
25
+ or os.environ.get("MISSION_DATA_DIR")
26
+ or Path(__file__).resolve().parent / "data"
27
+ )
28
+
29
+
30
+ def _deployment_mode() -> str:
31
+ return (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "trusted_team").strip().lower()
32
+
33
+
34
+ def proxy_token_configured() -> bool:
35
+ return bool(_loaded_proxy_token())
36
+
37
+
38
+ def _loaded_proxy_token() -> str:
39
+ global _TOKEN, _TOKEN_PATH
40
+ if _TOKEN is not None:
41
+ return _TOKEN
42
+ env = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
43
+ if env:
44
+ _TOKEN = env
45
+ return _TOKEN
46
+ path = _data_dir() / PROXY_TOKEN_FILE
47
+ _TOKEN_PATH = path
48
+ if path.is_file():
49
+ _TOKEN = path.read_text(encoding="utf-8").strip()
50
+ return _TOKEN
51
+ _TOKEN = ""
52
+ return ""
53
+
54
+
55
+ def _sync_shared_proxy_token(token: str) -> None:
56
+ value = str(token or "").strip()
57
+ if not value:
58
+ return
59
+ try:
60
+ SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
61
+ SHARED_PROXY_TOKEN_PATH.write_text(value + "\n", encoding="utf-8")
62
+ except OSError:
63
+ pass
64
+
65
+
66
+ def bootstrap_proxy_token(*, allow_generate_file: bool = True) -> str:
67
+ """Load proxy token from env or data dir; optionally create file for local dogfood."""
68
+ global _TOKEN
69
+ existing = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
70
+ if existing:
71
+ _TOKEN = existing
72
+ _sync_shared_proxy_token(existing)
73
+ return existing
74
+ path = _data_dir() / PROXY_TOKEN_FILE
75
+ if path.is_file():
76
+ _TOKEN = path.read_text(encoding="utf-8").strip()
77
+ _sync_shared_proxy_token(_TOKEN)
78
+ return _TOKEN
79
+ if not allow_generate_file or _deployment_mode() == "public_multi_user":
80
+ _TOKEN = ""
81
+ return ""
82
+ token = secrets.token_urlsafe(32)
83
+ path.parent.mkdir(parents=True, exist_ok=True)
84
+ path.write_text(token + "\n", encoding="utf-8")
85
+ # ponytail: gateway reads same token from shared compose volume when env unset
86
+ try:
87
+ SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
88
+ SHARED_PROXY_TOKEN_PATH.write_text(token + "\n", encoding="utf-8")
89
+ except OSError:
90
+ pass
91
+ _TOKEN = token
92
+ return token
93
+
94
+
95
+ def reset_proxy_token_for_tests() -> None:
96
+ global _TOKEN, _TOKEN_PATH
97
+ _TOKEN = None
98
+ _TOKEN_PATH = None
99
+
100
+
101
+ def _client_ip(handler: BaseHTTPRequestHandler) -> str:
102
+ return str(handler.client_address[0] if handler.client_address else "").strip()
103
+
104
+
105
+ def is_internal_client(host: str) -> bool:
106
+ """Private/docker callers only — not routable public addresses."""
107
+ addr = str(host or "").strip()
108
+ if not addr:
109
+ return False
110
+ if addr in {"127.0.0.1", "::1", "localhost"}:
111
+ return True
112
+ try:
113
+ ip = ipaddress.ip_address(addr.split("%", 1)[0])
114
+ except ValueError:
115
+ return False
116
+ return ip.is_private or ip.is_loopback
117
+
118
+
119
+ def _header_token(handler: BaseHTTPRequestHandler) -> str:
120
+ return str(handler.headers.get(PROXY_TOKEN_HEADER) or "").strip()
121
+
122
+
123
+ def authorize_internal_proxy(handler: BaseHTTPRequestHandler) -> tuple[bool, str]:
124
+ """Return (ok, error_code)."""
125
+ if not is_internal_client(_client_ip(handler)):
126
+ return False, "internal only"
127
+ expected = _loaded_proxy_token()
128
+ if not expected:
129
+ return True, ""
130
+ got = _header_token(handler)
131
+ if not got or not hmac.compare_digest(got, expected):
132
+ return False, "proxy token required"
133
+ return True, ""
134
+
135
+
136
+ if __name__ == "__main__":
137
+ reset_proxy_token_for_tests()
138
+ os.environ[PROXY_TOKEN_ENV] = "test-proxy-token"
139
+ assert proxy_token_configured()
140
+
141
+ class _H:
142
+ client_address = ("172.19.0.2", 1234)
143
+ headers = {PROXY_TOKEN_HEADER: "test-proxy-token"}
144
+
145
+ ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
146
+ assert ok and not err
147
+ _H.headers = {}
148
+ ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
149
+ assert not ok and err == "proxy token required"
150
+ print("internal_proxy_auth ok")
@@ -0,0 +1,277 @@
1
+ """Internal LLM proxy — Hermes sends lease tokens; API vault supplies upstream keys."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import socket
9
+ import ssl
10
+ import urllib.error
11
+ import urllib.request
12
+ from typing import Any, Callable
13
+ from http.server import BaseHTTPRequestHandler
14
+
15
+ import internal_proxy_auth
16
+ import turn_credentials
17
+
18
+ LEASE_PREFIX = turn_credentials.LEASE_PREFIX
19
+
20
+ UPSTREAM_BASE: dict[str, str] = {
21
+ "openrouter": "https://openrouter.ai/api/v1",
22
+ "openai": "https://api.openai.com/v1",
23
+ "anthropic": "https://api.anthropic.com",
24
+ "google": "https://generativelanguage.googleapis.com/v1beta",
25
+ "deepseek": "https://api.deepseek.com/v1",
26
+ }
27
+
28
+ PROXY_PATH_RE = re.compile(r"^/internal/llm/([a-z0-9_-]+)(/.*)?$", re.IGNORECASE)
29
+
30
+
31
+ def normalize_upstream_path(base: str, subpath: str) -> str:
32
+ """Drop a leading /v1 when upstream base already ends with /v1.
33
+
34
+ Hermes uses model.base_url …/internal/llm/openrouter/v1; the OpenAI client
35
+ then requests …/v1/chat/completions, which we forward as subpath /v1/….
36
+ """
37
+ path = subpath if str(subpath or "").startswith("/") else f"/{subpath or ''}"
38
+ base_norm = str(base or "").rstrip("/")
39
+ if path == "/v1":
40
+ return ""
41
+ if path.startswith("/v1/") and base_norm.endswith("/v1"):
42
+ return path[3:]
43
+ return path
44
+
45
+
46
+ def is_internal_client(host: str) -> bool:
47
+ """Allow docker/private callers only — not public browser origins."""
48
+ return internal_proxy_auth.is_internal_client(host)
49
+
50
+
51
+ def authorize_internal_proxy(handler: BaseHTTPRequestHandler) -> tuple[bool, str]:
52
+ return internal_proxy_auth.authorize_internal_proxy(handler)
53
+
54
+
55
+ def extract_profile_slug(headers: dict[str, str]) -> str:
56
+ return str(
57
+ headers.get(internal_proxy_auth.PROFILE_HEADER)
58
+ or headers.get("x-workframe-profile")
59
+ or ""
60
+ ).strip()
61
+
62
+
63
+ def validate_lease_profile(
64
+ lease: dict[str, Any],
65
+ headers: dict[str, str],
66
+ ) -> tuple[bool, str, int]:
67
+ """Bind bearer lease to calling Hermes profile (0022 N2 / 0023 C1)."""
68
+ want = str(lease.get("profile_slug") or "").strip()
69
+ if not want:
70
+ return True, "", 0
71
+ got = extract_profile_slug(headers)
72
+ if not got:
73
+ return False, "profile header required", 403
74
+ if got != want:
75
+ return False, "profile mismatch", 403
76
+ return True, "", 0
77
+
78
+
79
+ def extract_bearer(headers: dict[str, str]) -> str:
80
+ auth = str(headers.get("Authorization") or headers.get("authorization") or "").strip()
81
+ if auth.lower().startswith("bearer "):
82
+ return auth[7:].strip()
83
+ api_key = str(headers.get("X-Api-Key") or headers.get("x-api-key") or "").strip()
84
+ if api_key:
85
+ return api_key
86
+ return ""
87
+
88
+
89
+ def upstream_auth_header(provider: str, secret: str) -> dict[str, str]:
90
+ provider = str(provider or "").strip().lower()
91
+ secret = str(secret or "").strip()
92
+ if provider == "anthropic":
93
+ return {"x-api-key": secret, "anthropic-version": "2023-06-01"}
94
+ if provider == "google":
95
+ return {"x-goog-api-key": secret}
96
+ return {"Authorization": f"Bearer {secret}"}
97
+
98
+
99
+ def _error_response(status: int, error: str) -> tuple[int, dict[str, str], bytes]:
100
+ return status, {"Content-Type": "application/json"}, json.dumps({"error": error}).encode()
101
+
102
+
103
+ def _build_upstream_request(
104
+ provider: str,
105
+ subpath: str,
106
+ method: str,
107
+ headers: dict[str, str],
108
+ body: bytes | None,
109
+ *,
110
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
111
+ ) -> tuple[urllib.request.Request | None, tuple[int, dict[str, str], bytes] | None]:
112
+ provider = str(provider or "").strip().lower()
113
+ base = UPSTREAM_BASE.get(provider)
114
+ if not base:
115
+ return None, _error_response(404, "unknown provider")
116
+
117
+ token = extract_bearer(headers)
118
+ lease = turn_credentials.validate_lease(token)
119
+ if not lease:
120
+ return None, _error_response(401, "invalid lease")
121
+ if str(lease.get("provider") or "").lower() != provider:
122
+ return None, _error_response(403, "provider mismatch")
123
+
124
+ ok_profile, profile_err, profile_status = validate_lease_profile(lease, headers)
125
+ if not ok_profile:
126
+ return None, _error_response(profile_status, profile_err)
127
+
128
+ _env_var, secret = turn_credentials.resolve_lease_secret(lease, resolve_secret)
129
+ if not secret:
130
+ return None, _error_response(402, "no credential")
131
+
132
+ path = normalize_upstream_path(base, subpath)
133
+ url = f"{base.rstrip('/')}{path}"
134
+ upstream_headers = {
135
+ k: v
136
+ for k, v in headers.items()
137
+ if k.lower() not in {
138
+ "host",
139
+ "connection",
140
+ "content-length",
141
+ "authorization",
142
+ "x-api-key",
143
+ "x-goog-api-key",
144
+ }
145
+ }
146
+ upstream_headers.update(upstream_auth_header(provider, secret))
147
+
148
+ req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
149
+ return req, None
150
+
151
+
152
+ def forward_request(
153
+ provider: str,
154
+ subpath: str,
155
+ method: str,
156
+ headers: dict[str, str],
157
+ body: bytes | None,
158
+ *,
159
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
160
+ ) -> tuple[int, dict[str, str], bytes]:
161
+ req, error = _build_upstream_request(
162
+ provider,
163
+ subpath,
164
+ method,
165
+ headers,
166
+ body,
167
+ resolve_secret=resolve_secret,
168
+ )
169
+ if error:
170
+ return error
171
+ assert req is not None
172
+ try:
173
+ with urllib.request.urlopen(req, timeout=600) as resp:
174
+ resp_body = resp.read()
175
+ out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
176
+ return resp.status, out_headers, resp_body
177
+ except urllib.error.HTTPError as exc:
178
+ raw = exc.read()
179
+ out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
180
+ return exc.code, out_headers, raw
181
+
182
+
183
+ def stream_request_to_handler(
184
+ handler: BaseHTTPRequestHandler,
185
+ provider: str,
186
+ subpath: str,
187
+ method: str,
188
+ headers: dict[str, str],
189
+ body: bytes | None,
190
+ *,
191
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
192
+ ) -> None:
193
+ req, error = _build_upstream_request(
194
+ provider,
195
+ subpath,
196
+ method,
197
+ headers,
198
+ body,
199
+ resolve_secret=resolve_secret,
200
+ )
201
+ if error:
202
+ status, out_headers, resp_body = error
203
+ handler.send_response(status)
204
+ for key, value in out_headers.items():
205
+ handler.send_header(key, value)
206
+ handler.end_headers()
207
+ handler.wfile.write(resp_body)
208
+ return
209
+
210
+ assert req is not None
211
+ try:
212
+ with urllib.request.urlopen(req, timeout=600) as resp:
213
+ content_type = resp.headers.get("Content-Type", "application/octet-stream")
214
+ handler.send_response(resp.status)
215
+ handler.send_header("Content-Type", content_type)
216
+ handler.end_headers()
217
+ if "text/event-stream" in content_type.lower():
218
+ while True:
219
+ line = resp.readline()
220
+ if not line:
221
+ break
222
+ handler.wfile.write(line)
223
+ handler.wfile.flush()
224
+ else:
225
+ while True:
226
+ chunk = resp.read(8192)
227
+ if not chunk:
228
+ break
229
+ handler.wfile.write(chunk)
230
+ handler.wfile.flush()
231
+ except urllib.error.HTTPError as exc:
232
+ raw = exc.read()
233
+ handler.send_response(exc.code)
234
+ handler.send_header("Content-Type", exc.headers.get("Content-Type", "application/json"))
235
+ handler.end_headers()
236
+ handler.wfile.write(raw)
237
+ except urllib.error.URLError as exc:
238
+ handler.send_response(502)
239
+ handler.send_header("Content-Type", "application/json")
240
+ handler.end_headers()
241
+ handler.wfile.write(json.dumps({"error": f"upstream unavailable: {exc}"}).encode())
242
+
243
+
244
+ def handle_proxy_request(
245
+ handler: BaseHTTPRequestHandler,
246
+ path: str,
247
+ method: str,
248
+ body: bytes | None,
249
+ *,
250
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
251
+ ) -> bool:
252
+ """Return True if handled."""
253
+ ok, err = authorize_internal_proxy(handler)
254
+ if not ok:
255
+ handler.send_response(403)
256
+ handler.send_header("Content-Type", "application/json")
257
+ handler.end_headers()
258
+ handler.wfile.write(json.dumps({"error": err}).encode())
259
+ return True
260
+
261
+ match = PROXY_PATH_RE.match(path)
262
+ if not match:
263
+ return False
264
+
265
+ provider = match.group(1).lower()
266
+ subpath = match.group(2) or "/"
267
+ headers = {k: v for k, v in handler.headers.items()}
268
+ stream_request_to_handler(
269
+ handler,
270
+ provider,
271
+ subpath,
272
+ method,
273
+ headers,
274
+ body,
275
+ resolve_secret=resolve_secret,
276
+ )
277
+ return True
@@ -0,0 +1,108 @@
1
+ """Minimal OIDC id_token verification (RS256 + JWKS) — no extra dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import time
8
+ import urllib.error
9
+ import urllib.request
10
+ from typing import Any
11
+
12
+ from cryptography.hazmat.primitives import hashes
13
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
14
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
15
+
16
+ _JWKS_CACHE: dict[str, tuple[float, dict[str, Any]]] = {}
17
+ _JWKS_TTL = 3600.0
18
+
19
+
20
+ def _b64url_decode(raw: str) -> bytes:
21
+ padded = raw + "=" * (-len(raw) % 4)
22
+ return base64.urlsafe_b64decode(padded.encode("ascii"))
23
+
24
+
25
+ def _jwk_to_rsa(key: dict[str, Any]) -> RSAPublicKey:
26
+ n = int.from_bytes(_b64url_decode(str(key["n"])), "big")
27
+ e = int.from_bytes(_b64url_decode(str(key["e"])), "big")
28
+ return rsa.RSAPublicNumbers(e, n).public_key()
29
+
30
+
31
+ def _fetch_jwks(jwks_url: str) -> dict[str, Any]:
32
+ now = time.time()
33
+ cached = _JWKS_CACHE.get(jwks_url)
34
+ if cached and cached[0] > now:
35
+ return cached[1]
36
+ req = urllib.request.Request(jwks_url, headers={"Accept": "application/json"})
37
+ with urllib.request.urlopen(req, timeout=15) as resp:
38
+ payload = json.loads(resp.read().decode("utf-8"))
39
+ if not isinstance(payload, dict):
40
+ raise ValueError("invalid jwks")
41
+ _JWKS_CACHE[jwks_url] = (now + _JWKS_TTL, payload)
42
+ return payload
43
+
44
+
45
+ def verify_rs256_id_token(
46
+ token: str,
47
+ *,
48
+ audience: str,
49
+ issuer: str | tuple[str, ...] = (),
50
+ jwks_url: str,
51
+ clock_skew: int = 60,
52
+ ) -> dict[str, Any]:
53
+ parts = str(token or "").split(".")
54
+ if len(parts) != 3:
55
+ raise ValueError("invalid id_token")
56
+ header = json.loads(_b64url_decode(parts[0]))
57
+ payload = json.loads(_b64url_decode(parts[1]))
58
+ if str(header.get("alg") or "") != "RS256":
59
+ raise ValueError("unsupported jwt alg")
60
+ kid = str(header.get("kid") or "")
61
+ jwks = _fetch_jwks(jwks_url)
62
+ keys = jwks.get("keys") if isinstance(jwks.get("keys"), list) else []
63
+ pub: RSAPublicKey | None = None
64
+ for entry in keys:
65
+ if not isinstance(entry, dict):
66
+ continue
67
+ if kid and str(entry.get("kid") or "") != kid:
68
+ continue
69
+ if str(entry.get("kty") or "") != "RSA":
70
+ continue
71
+ pub = _jwk_to_rsa(entry)
72
+ break
73
+ if pub is None:
74
+ raise ValueError("jwks key not found")
75
+ signing_input = f"{parts[0]}.{parts[1]}".encode("ascii")
76
+ signature = _b64url_decode(parts[2])
77
+ pub.verify(signature, signing_input, padding.PKCS1v15(), hashes.SHA256())
78
+
79
+ now = int(time.time())
80
+ exp = int(payload.get("exp") or 0)
81
+ if exp and now > exp + clock_skew:
82
+ raise ValueError("id_token expired")
83
+ iat = int(payload.get("iat") or 0)
84
+ if iat and now + clock_skew < iat:
85
+ raise ValueError("id_token not yet valid")
86
+
87
+ iss = str(payload.get("iss") or "")
88
+ allowed_iss = issuer or ()
89
+ if allowed_iss and iss not in allowed_iss:
90
+ raise ValueError("issuer mismatch")
91
+
92
+ aud = payload.get("aud")
93
+ aud_ok = audience in (aud if isinstance(aud, list) else [aud])
94
+ if not aud_ok:
95
+ raise ValueError("audience mismatch")
96
+
97
+ if not isinstance(payload, dict):
98
+ raise ValueError("invalid claims")
99
+ return payload
100
+
101
+
102
+ def verify_google_id_token(token: str, client_id: str) -> dict[str, Any]:
103
+ return verify_rs256_id_token(
104
+ token,
105
+ audience=str(client_id or "").strip(),
106
+ issuer=("https://accounts.google.com", "accounts.google.com"),
107
+ jwks_url="https://www.googleapis.com/oauth2/v3/certs",
108
+ )
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@workframe/workframe-api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "python3 server.py",
8
+ "start": "python3 server.py",
9
+ "test": "python3 -m unittest discover -s tests",
10
+ "typecheck": "python3 -m py_compile server.py zk_auth.py email_sender.py",
11
+ "build": "python3 -m py_compile server.py zk_auth.py email_sender.py"
12
+ }
13
+ }