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,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")