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,159 +1,159 @@
1
- """Vault master key (KEK) — in-memory only; separate from ZK_AUTH_ENCRYPTION_KEY."""
2
-
3
- from __future__ import annotations
4
-
5
- import base64
6
- import hashlib
7
- import json
8
- import os
9
- import secrets
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
-
15
- DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
16
- VAULT_KEK_FILE = DATA_DIR / ".vault_kek"
17
- META_TABLE = "vault_meta"
18
-
19
- _KEK: bytes | None = None
20
- _PBKDF2_ROUNDS = 200_000
21
-
22
-
23
- def _b64e(raw: bytes) -> str:
24
- return base64.b64encode(raw).decode("ascii")
25
-
26
-
27
- def _b64d(value: str) -> bytes:
28
- raw = base64.b64decode(str(value or "").strip())
29
- if len(raw) != 32:
30
- raise ValueError("vault KEK must be 32 bytes")
31
- return raw
32
-
33
-
34
- def _derive_wrap_key(passphrase: str, salt: bytes) -> bytes:
35
- phrase = str(passphrase or "")
36
- if len(phrase) < 12:
37
- raise ValueError("passphrase must be at least 12 characters")
38
- return hashlib.pbkdf2_hmac("sha256", phrase.encode("utf-8"), salt, _PBKDF2_ROUNDS, dklen=32)
39
-
40
-
41
- def _aes_gcm_encrypt(key: bytes, plaintext: bytes) -> dict[str, str]:
42
- iv = os.urandom(12)
43
- ct = AESGCM(key).encrypt(iv, plaintext, None)
44
- return {
45
- "v": 1,
46
- "alg": "AES-256-GCM",
47
- "iv": _b64e(iv),
48
- "tag": _b64e(ct[-16:]),
49
- "ciphertext": _b64e(ct[:-16]),
50
- }
51
-
52
-
53
- def _aes_gcm_decrypt(key: bytes, payload: dict[str, Any]) -> bytes:
54
- if int(payload.get("v") or 0) != 1 or str(payload.get("alg") or "") != "AES-256-GCM":
55
- raise ValueError("unsupported wrap payload")
56
- iv = base64.b64decode(str(payload["iv"]))
57
- tag = base64.b64decode(str(payload["tag"]))
58
- ct = base64.b64decode(str(payload["ciphertext"]))
59
- return AESGCM(key).decrypt(iv, ct + tag, None)
60
-
61
-
62
- def kek_in_memory() -> bool:
63
- return _KEK is not None
64
-
65
-
66
- def get_kek() -> bytes:
67
- if _KEK is None:
68
- raise RuntimeError("vault_sealed")
69
- return _KEK
70
-
71
-
72
- def set_kek(raw: bytes) -> None:
73
- global _KEK
74
- if len(raw) != 32:
75
- raise ValueError("KEK must be 32 bytes")
76
- _KEK = bytes(raw)
77
-
78
-
79
- def clear_kek() -> None:
80
- global _KEK
81
- _KEK = None
82
-
83
-
84
- def load_kek_from_env() -> bool:
85
- raw = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
86
- if not raw:
87
- return False
88
- set_kek(_b64d(raw))
89
- return True
90
-
91
-
92
- def load_kek_from_file() -> bool:
93
- if not VAULT_KEK_FILE.is_file():
94
- return False
95
- try:
96
- text = VAULT_KEK_FILE.read_text(encoding="utf-8").strip()
97
- payload = json.loads(text) if text.startswith("{") else {"key": text}
98
- key_b64 = str(payload.get("key") or text).strip()
99
- set_kek(_b64d(key_b64))
100
- return True
101
- except Exception:
102
- return False
103
-
104
-
105
- def persist_kek_file() -> None:
106
- """Persist in-memory KEK for single-tenant bootstrap (chmod 600)."""
107
- if _KEK is None:
108
- return
109
- DATA_DIR.mkdir(parents=True, exist_ok=True)
110
- VAULT_KEK_FILE.write_text(
111
- json.dumps({"v": 1, "key": _b64e(_KEK)}),
112
- encoding="utf-8",
113
- )
114
- try:
115
- os.chmod(VAULT_KEK_FILE, 0o600)
116
- except OSError:
117
- pass
118
-
119
-
120
- def generate_and_persist_kek() -> bytes:
121
- set_kek(os.urandom(32))
122
- persist_kek_file()
123
- return get_kek()
124
-
125
-
126
- def wrap_kek_for_passphrase(passphrase: str) -> tuple[str, str]:
127
- """Return (salt_b64, wrapped_kek_json) for vault_meta."""
128
- if _KEK is None:
129
- raise RuntimeError("vault_sealed")
130
- salt = os.urandom(16)
131
- wrap_key = _derive_wrap_key(passphrase, salt)
132
- wrapped = _aes_gcm_encrypt(wrap_key, _KEK)
133
- return _b64e(salt), json.dumps(wrapped)
134
-
135
-
136
- def unwrap_kek_from_passphrase(passphrase: str, salt_b64: str, wrapped_json: str) -> bytes:
137
- salt = base64.b64decode(str(salt_b64 or "").strip())
138
- wrap_key = _derive_wrap_key(passphrase, salt)
139
- payload = json.loads(wrapped_json)
140
- return _aes_gcm_decrypt(wrap_key, payload)
141
-
142
-
143
- def unseal_for_tests() -> None:
144
- """Tests: deterministic KEK without touching disk."""
145
- seed = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
146
- if seed:
147
- set_kek(_b64d(seed))
148
- return
149
- set_kek(hashlib.sha256(b"workframe-test-vault-kek").digest())
150
-
151
-
152
- if __name__ == "__main__":
153
- generate_and_persist_kek()
154
- assert kek_in_memory()
155
- salt, wrapped = wrap_kek_for_passphrase("test-passphrase-12")
156
- clear_kek()
157
- set_kek(unwrap_kek_from_passphrase("test-passphrase-12", salt, wrapped))
158
- assert kek_in_memory()
159
- print("vault_kek ok")
1
+ """Vault master key (KEK) — in-memory only; separate from ZK_AUTH_ENCRYPTION_KEY."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import secrets
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
+
15
+ DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
16
+ VAULT_KEK_FILE = DATA_DIR / ".vault_kek"
17
+ META_TABLE = "vault_meta"
18
+
19
+ _KEK: bytes | None = None
20
+ _PBKDF2_ROUNDS = 200_000
21
+
22
+
23
+ def _b64e(raw: bytes) -> str:
24
+ return base64.b64encode(raw).decode("ascii")
25
+
26
+
27
+ def _b64d(value: str) -> bytes:
28
+ raw = base64.b64decode(str(value or "").strip())
29
+ if len(raw) != 32:
30
+ raise ValueError("vault KEK must be 32 bytes")
31
+ return raw
32
+
33
+
34
+ def _derive_wrap_key(passphrase: str, salt: bytes) -> bytes:
35
+ phrase = str(passphrase or "")
36
+ if len(phrase) < 12:
37
+ raise ValueError("passphrase must be at least 12 characters")
38
+ return hashlib.pbkdf2_hmac("sha256", phrase.encode("utf-8"), salt, _PBKDF2_ROUNDS, dklen=32)
39
+
40
+
41
+ def _aes_gcm_encrypt(key: bytes, plaintext: bytes) -> dict[str, str]:
42
+ iv = os.urandom(12)
43
+ ct = AESGCM(key).encrypt(iv, plaintext, None)
44
+ return {
45
+ "v": 1,
46
+ "alg": "AES-256-GCM",
47
+ "iv": _b64e(iv),
48
+ "tag": _b64e(ct[-16:]),
49
+ "ciphertext": _b64e(ct[:-16]),
50
+ }
51
+
52
+
53
+ def _aes_gcm_decrypt(key: bytes, payload: dict[str, Any]) -> bytes:
54
+ if int(payload.get("v") or 0) != 1 or str(payload.get("alg") or "") != "AES-256-GCM":
55
+ raise ValueError("unsupported wrap payload")
56
+ iv = base64.b64decode(str(payload["iv"]))
57
+ tag = base64.b64decode(str(payload["tag"]))
58
+ ct = base64.b64decode(str(payload["ciphertext"]))
59
+ return AESGCM(key).decrypt(iv, ct + tag, None)
60
+
61
+
62
+ def kek_in_memory() -> bool:
63
+ return _KEK is not None
64
+
65
+
66
+ def get_kek() -> bytes:
67
+ if _KEK is None:
68
+ raise RuntimeError("vault_sealed")
69
+ return _KEK
70
+
71
+
72
+ def set_kek(raw: bytes) -> None:
73
+ global _KEK
74
+ if len(raw) != 32:
75
+ raise ValueError("KEK must be 32 bytes")
76
+ _KEK = bytes(raw)
77
+
78
+
79
+ def clear_kek() -> None:
80
+ global _KEK
81
+ _KEK = None
82
+
83
+
84
+ def load_kek_from_env() -> bool:
85
+ raw = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
86
+ if not raw:
87
+ return False
88
+ set_kek(_b64d(raw))
89
+ return True
90
+
91
+
92
+ def load_kek_from_file() -> bool:
93
+ if not VAULT_KEK_FILE.is_file():
94
+ return False
95
+ try:
96
+ text = VAULT_KEK_FILE.read_text(encoding="utf-8").strip()
97
+ payload = json.loads(text) if text.startswith("{") else {"key": text}
98
+ key_b64 = str(payload.get("key") or text).strip()
99
+ set_kek(_b64d(key_b64))
100
+ return True
101
+ except Exception:
102
+ return False
103
+
104
+
105
+ def persist_kek_file() -> None:
106
+ """Persist in-memory KEK for single-tenant bootstrap (chmod 600)."""
107
+ if _KEK is None:
108
+ return
109
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
110
+ VAULT_KEK_FILE.write_text(
111
+ json.dumps({"v": 1, "key": _b64e(_KEK)}),
112
+ encoding="utf-8",
113
+ )
114
+ try:
115
+ os.chmod(VAULT_KEK_FILE, 0o600)
116
+ except OSError:
117
+ pass
118
+
119
+
120
+ def generate_and_persist_kek() -> bytes:
121
+ set_kek(os.urandom(32))
122
+ persist_kek_file()
123
+ return get_kek()
124
+
125
+
126
+ def wrap_kek_for_passphrase(passphrase: str) -> tuple[str, str]:
127
+ """Return (salt_b64, wrapped_kek_json) for vault_meta."""
128
+ if _KEK is None:
129
+ raise RuntimeError("vault_sealed")
130
+ salt = os.urandom(16)
131
+ wrap_key = _derive_wrap_key(passphrase, salt)
132
+ wrapped = _aes_gcm_encrypt(wrap_key, _KEK)
133
+ return _b64e(salt), json.dumps(wrapped)
134
+
135
+
136
+ def unwrap_kek_from_passphrase(passphrase: str, salt_b64: str, wrapped_json: str) -> bytes:
137
+ salt = base64.b64decode(str(salt_b64 or "").strip())
138
+ wrap_key = _derive_wrap_key(passphrase, salt)
139
+ payload = json.loads(wrapped_json)
140
+ return _aes_gcm_decrypt(wrap_key, payload)
141
+
142
+
143
+ def unseal_for_tests() -> None:
144
+ """Tests: deterministic KEK without touching disk."""
145
+ seed = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
146
+ if seed:
147
+ set_kek(_b64d(seed))
148
+ return
149
+ set_kek(hashlib.sha256(b"workframe-test-vault-kek").digest())
150
+
151
+
152
+ if __name__ == "__main__":
153
+ generate_and_persist_kek()
154
+ assert kek_in_memory()
155
+ salt, wrapped = wrap_kek_for_passphrase("test-passphrase-12")
156
+ clear_kek()
157
+ set_kek(unwrap_kek_from_passphrase("test-passphrase-12", salt, wrapped))
158
+ assert kek_in_memory()
159
+ print("vault_kek ok")