create-workframe 0.1.11 → 0.1.12

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 (100) hide show
  1. package/README.md +1 -1
  2. package/docs/security.md +85 -0
  3. package/package.json +1 -1
  4. package/scripts/sync-canonical-to-package.mjs +8 -0
  5. package/scripts/verify-public-deploy.sh +59 -3
  6. package/workframe-api/openrouter_catalog.py +4 -4
  7. package/workframe-api/package.json +2 -2
  8. package/workframe-api/profile_config_yaml.py +126 -0
  9. package/workframe-api/route_registry.py +330 -0
  10. package/workframe-api/run-typecheck.mjs +25 -0
  11. package/workframe-api/server.py +1523 -1392
  12. package/workframe-api/test_model_surface_consistency.py +116 -0
  13. package/workframe-api/test_profile_model_yaml.py +99 -0
  14. package/workframe-api/test_public_routes.py +41 -0
  15. package/workframe-api/test_route_registry.py +109 -0
  16. package/workframe-api/time-bind-steps.py +75 -0
  17. package/workframe-api/time-perf.py +81 -0
  18. package/workframe-api/time-warm.py +107 -0
  19. package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-B0OFRGmJ.js} +1 -1
  20. package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +1 -0
  21. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DeBNltHS.js} +1 -1
  22. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-BDhCHu7I.js} +1 -1
  23. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-olcDYvtI.js} +1 -1
  24. package/workframe-ui/public/assets/channel-fNHbDpV4.js +1 -0
  25. package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-D0obCBn_.js} +1 -1
  26. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DX93f-ZZ.js} +1 -1
  27. package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-BX9B5wUs.js} +1 -1
  28. package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-C4DGc6RO.js} +1 -1
  29. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-Crlja9Lf.js} +1 -1
  30. package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-JYSm3szI.js} +1 -1
  31. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-_KslxVEl.js} +1 -1
  32. package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-CpTO-jU8.js} +1 -1
  33. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-JGUkCmbI.js} +1 -1
  34. package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-BscBwGaC.js} +1 -1
  35. package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-5ZTZ0bpX.js} +1 -1
  36. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-DxvitYbW.js} +1 -1
  37. package/workframe-ui/public/assets/{chunk-LZXEDZCA-CsRdBqUB.js → chunk-LZXEDZCA-DMfdMUwz.js} +2 -2
  38. package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-Cayl0iV9.js} +1 -1
  39. package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-_7dyBR5G.js} +1 -1
  40. package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O--xTpD0yi.js} +1 -1
  41. package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +1 -0
  42. package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +1 -0
  43. package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-CwxJQaeK.js} +1 -1
  44. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +1 -0
  45. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +1 -0
  46. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-CCspENYv.js} +1 -1
  47. package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Dv8V6R60.js} +1 -1
  48. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-Da48hFPT.js} +1 -1
  49. package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-dTihc7-3.js} +1 -1
  50. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-D_Jqu699.js} +1 -1
  51. package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-Bt8PtEaZ.js} +1 -1
  52. package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-BtmPjlh0.js} +1 -1
  53. package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-Cz2turIT.js} +1 -1
  54. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-BxMGCflX.js} +1 -1
  55. package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +1 -0
  56. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-ivyroIZt.js} +1 -1
  57. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-B9llwShx.js} +1 -1
  58. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-BFQHk4bw.js} +1 -1
  59. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-ursegLbc.js} +1 -1
  60. package/workframe-ui/public/assets/index-8CuZDEIG.css +1 -0
  61. package/workframe-ui/public/assets/index-xS9lFekI.js +129 -0
  62. package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-DQpifAsB.js} +1 -1
  63. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-C16XP2Q7.js} +1 -1
  64. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-CiJoz7rP.js} +1 -1
  65. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-CVwEvvUL.js} +1 -1
  66. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-CWQ6hX5i.js} +1 -1
  67. package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-CgQsmU35.js} +1 -1
  68. package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-Bxbc77dL.js} +1 -1
  69. package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-C_jMeF0a.js} +2 -2
  70. package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-4RKV9MZP.js} +3 -3
  71. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-DZ7y7Sz0.js} +1 -1
  72. package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-B9h2I7Vq.js} +1 -1
  73. package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-B2mP5uHm.js} +1 -1
  74. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-DyYKyKiP.js} +1 -1
  75. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-CL_6nej-.js} +1 -1
  76. package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-CNKNDQ7S.js} +1 -1
  77. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-eWU_mhFa.js} +1 -1
  78. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-DrjcXvEQ.js} +1 -1
  79. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-BHWR1xTJ.js} +1 -1
  80. package/workframe-ui/public/assets/{src-CY_By-r7.js → src-DfJB8kV1.js} +1 -1
  81. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-BGJbSd3I.js} +1 -1
  82. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +1 -0
  83. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-DkwoCjiZ.js} +1 -1
  84. package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-GxfhiqoW.js} +1 -1
  85. package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-DxrOsars.js} +1 -1
  86. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-BsWhpUfr.js} +1 -1
  87. package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-CrFZWMHS.js} +1 -1
  88. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DV8Xpf5I.js} +1 -1
  89. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-DqMqnwdZ.js} +1 -1
  90. package/workframe-ui/public/index.html +4 -5
  91. package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
  92. package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
  93. package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
  94. package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
  95. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
  96. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
  97. package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
  98. package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
  99. package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
  100. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
@@ -0,0 +1,330 @@
1
+ """Declarative API route registry (WF-037).
2
+
3
+ Deny-by-default auth tiers plus batched handler dispatch; handler bodies live on server.Handler.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+
12
+ class AuthLevel(str, Enum):
13
+ PUBLIC = "public"
14
+ SINGLE_USER_GET = "single_user_get"
15
+ AUTH_FLOW = "auth_flow"
16
+ INSTALL = "install"
17
+ SESSION = "session"
18
+
19
+
20
+ GET_ALWAYS_PUBLIC_ROUTES = frozenset({
21
+ "/api/meta",
22
+ "/api/health",
23
+ "/api/setup/status",
24
+ "/api/install/status",
25
+ "/api/public/site-meta",
26
+ "/api/public/link-preview",
27
+ "/api/public/manifest.webmanifest",
28
+ "/api/auth/google/callback",
29
+ })
30
+
31
+ GET_SINGLE_USER_PUBLIC_ROUTES = frozenset({
32
+ "/api/files/read",
33
+ "/api/files/raw",
34
+ "/api/files/tree",
35
+ "/api/files/list",
36
+ "/api/files/state",
37
+ "/api/board",
38
+ "/api/content",
39
+ "/api/content/get",
40
+ "/api/agents",
41
+ "/api/snapshot",
42
+ "/api/hermes/usage",
43
+ "/api/hermes/profile",
44
+ "/api/hermes/debug",
45
+ "/api/hermes/insights",
46
+ "/api/hermes/gquota",
47
+ "/api/hermes/models",
48
+ "/api/hermes/skills",
49
+ "/api/hermes/commands",
50
+ "/api/routes",
51
+ "/api/chat/session",
52
+ "/api/chat/resolve",
53
+ "/api/chat/messages",
54
+ "/api/profiles",
55
+ })
56
+
57
+ AUTH_FLOW_POST_ROUTES = frozenset({
58
+ "/api/auth/start",
59
+ "/api/auth/verify",
60
+ "/api/auth/logout",
61
+ "/api/auth/refresh",
62
+ "/api/auth/bootstrap",
63
+ "/api/setup",
64
+ "/api/auth/google/start",
65
+ "/api/auth/local-bootstrap",
66
+ })
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class Route:
70
+ method: str
71
+ path: str
72
+ handler: str
73
+ auth: AuthLevel
74
+
75
+
76
+ # Batch: data-read GET surfaces (handler methods on server.Handler).
77
+ ROUTES: tuple[Route, ...] = (
78
+ Route("GET", "/api/meta", "_route_get_meta", AuthLevel.PUBLIC),
79
+ Route("GET", "/api/health", "_route_get_health", AuthLevel.PUBLIC),
80
+ Route("GET", "/api/setup/status", "_route_get_setup_status", AuthLevel.PUBLIC),
81
+ Route("GET", "/api/install/status", "_route_get_install_status", AuthLevel.PUBLIC),
82
+ Route("GET", "/api/public/site-meta", "_route_get_public_site_meta", AuthLevel.PUBLIC),
83
+ Route("GET", "/api/public/link-preview", "_route_get_public_link_preview", AuthLevel.PUBLIC),
84
+ Route("GET", "/api/public/manifest.webmanifest", "_route_get_public_manifest", AuthLevel.PUBLIC),
85
+ Route("GET", "/api/agents", "_route_get_agents", AuthLevel.SINGLE_USER_GET),
86
+ Route("GET", "/api/snapshot", "_route_get_snapshot", AuthLevel.SINGLE_USER_GET),
87
+ Route("GET", "/api/activity/detail", "_route_get_activity_detail", AuthLevel.SESSION),
88
+ Route("GET", "/api/board", "_route_get_board", AuthLevel.SINGLE_USER_GET),
89
+ Route("GET", "/api/content", "_route_get_content", AuthLevel.SINGLE_USER_GET),
90
+ Route("GET", "/api/content/get", "_route_get_content_get", AuthLevel.SINGLE_USER_GET),
91
+ Route("GET", "/api/files/tree", "_route_get_files_tree", AuthLevel.SINGLE_USER_GET),
92
+ Route("GET", "/api/files/list", "_route_get_files_list", AuthLevel.SINGLE_USER_GET),
93
+ Route("GET", "/api/files/state", "_route_get_files_state", AuthLevel.SINGLE_USER_GET),
94
+ Route("GET", "/api/files/read", "_route_get_files_read", AuthLevel.SINGLE_USER_GET),
95
+ Route("GET", "/api/files/raw", "_route_get_files_raw", AuthLevel.SINGLE_USER_GET),
96
+ Route("GET", "/api/routes", "_route_get_routes", AuthLevel.SINGLE_USER_GET),
97
+ Route("GET", "/api/chat/messages", "_route_get_chat_messages", AuthLevel.SINGLE_USER_GET),
98
+ Route("GET", "/api/chat/session", "_route_get_chat_session", AuthLevel.SINGLE_USER_GET),
99
+ Route("GET", "/api/chat/resolve", "_route_get_chat_resolve", AuthLevel.SINGLE_USER_GET),
100
+ Route("GET", "/api/hermes/skills", "_route_get_hermes_skills", AuthLevel.SINGLE_USER_GET),
101
+ Route("GET", "/api/hermes/commands", "_route_get_hermes_commands", AuthLevel.SINGLE_USER_GET),
102
+ Route("GET", "/api/hermes/usage", "_route_get_hermes_usage", AuthLevel.SINGLE_USER_GET),
103
+ Route("GET", "/api/hermes/profile", "_route_get_hermes_profile", AuthLevel.SINGLE_USER_GET),
104
+ # Batch 3: session /me + hermes catalog GET surfaces
105
+ Route("GET", "/api/me", "_route_get_me", AuthLevel.SESSION),
106
+ Route("GET", "/api/me/onboarding", "_route_get_me_onboarding", AuthLevel.SESSION),
107
+ Route("GET", "/api/me/credentials", "_route_get_me_credentials", AuthLevel.SESSION),
108
+ Route("GET", "/api/me/providers", "_route_get_me_providers", AuthLevel.SESSION),
109
+ Route("GET", "/api/hermes/models", "_route_get_hermes_models", AuthLevel.SINGLE_USER_GET),
110
+ Route("GET", "/api/hermes/debug", "_route_get_hermes_debug", AuthLevel.SINGLE_USER_GET),
111
+ Route("GET", "/api/hermes/insights", "_route_get_hermes_insights", AuthLevel.SINGLE_USER_GET),
112
+ Route("GET", "/api/hermes/gquota", "_route_get_hermes_gquota", AuthLevel.SINGLE_USER_GET),
113
+ # Batch 4: session cohort + legacy user credentials
114
+ Route("GET", "/api/me/cohort", "_route_get_me_cohort", AuthLevel.SESSION),
115
+ Route("GET", "/api/user/credentials", "_route_get_user_credentials", AuthLevel.SESSION),
116
+ # Batch 2: auth-flow + hermes/chat + me credentials POST surfaces
117
+ Route("POST", "/api/auth/start", "_route_post_auth_start", AuthLevel.AUTH_FLOW),
118
+ Route("POST", "/api/auth/verify", "_route_post_auth_verify", AuthLevel.AUTH_FLOW),
119
+ Route("POST", "/api/auth/logout", "_route_post_auth_logout", AuthLevel.AUTH_FLOW),
120
+ Route("POST", "/api/auth/refresh", "_route_post_auth_refresh", AuthLevel.AUTH_FLOW),
121
+ Route("POST", "/api/auth/bootstrap", "_route_post_auth_bootstrap", AuthLevel.AUTH_FLOW),
122
+ Route("POST", "/api/auth/google/start", "_route_post_auth_google_start", AuthLevel.AUTH_FLOW),
123
+ Route("POST", "/api/auth/local-bootstrap", "_route_post_auth_local_bootstrap", AuthLevel.AUTH_FLOW),
124
+ Route("POST", "/api/setup", "_route_post_setup", AuthLevel.AUTH_FLOW),
125
+ Route("POST", "/api/hermes/model", "_route_post_hermes_model", AuthLevel.SESSION),
126
+ Route("POST", "/api/hermes/model/apply-default", "_route_post_hermes_model_apply_default", AuthLevel.SESSION),
127
+ Route("POST", "/api/hermes/fallback-chain", "_route_post_hermes_fallback_chain", AuthLevel.SESSION),
128
+ Route("POST", "/api/hermes/commands/exec", "_route_post_hermes_commands_exec", AuthLevel.SESSION),
129
+ Route("POST", "/api/hermes/gateway/exec", "_route_post_hermes_gateway_exec", AuthLevel.SESSION),
130
+ Route("POST", "/api/chat/stop", "_route_post_chat_stop", AuthLevel.SESSION),
131
+ Route("POST", "/api/chat/steer", "_route_post_chat_steer", AuthLevel.SESSION),
132
+ Route("POST", "/api/me/credentials", "_route_post_me_credentials", AuthLevel.SESSION),
133
+ Route("POST", "/api/me/telegram/link", "_route_post_me_telegram_link", AuthLevel.SESSION),
134
+ Route("POST", "/api/board", "_route_post_board", AuthLevel.SESSION),
135
+ # Batch 5: workspace mutation POST surfaces
136
+ Route("POST", "/api/board/update", "_route_post_board_update", AuthLevel.SESSION),
137
+ Route("POST", "/api/board/delete", "_route_post_board_delete", AuthLevel.SESSION),
138
+ Route("POST", "/api/content/save", "_route_post_content_save", AuthLevel.SESSION),
139
+ Route("POST", "/api/content/delete", "_route_post_content_delete", AuthLevel.SESSION),
140
+ Route("POST", "/api/files/write", "_route_post_files_write", AuthLevel.SESSION),
141
+ Route("POST", "/api/files/upload", "_route_post_files_upload", AuthLevel.SESSION),
142
+ Route("POST", "/api/chat/dispatch", "_route_post_chat_dispatch", AuthLevel.SESSION),
143
+ )
144
+
145
+ _ROUTES_BY_METHOD_PATH: dict[tuple[str, str], Route] = {
146
+ (r.method.upper(), r.path): r for r in ROUTES
147
+ }
148
+
149
+
150
+ INSTALL_ROUTE_METHODS: dict[str, frozenset[str]] = {
151
+ "GET": frozenset({
152
+ "/api/install/publish-hints",
153
+ "/api/install/url/test",
154
+ }),
155
+ "PATCH": frozenset({"/api/install/stack"}),
156
+ "POST": frozenset({
157
+ "/api/install/email/test",
158
+ "/api/install/complete",
159
+ "/api/install/url/test",
160
+ "/api/install/setup-https",
161
+ }),
162
+ }
163
+
164
+
165
+ def resolve_auth_level(method: str, path: str) -> AuthLevel:
166
+ """Map HTTP method + path to required auth level. All /api/* paths are covered."""
167
+ m = str(method or "GET").upper()
168
+ p = str(path or "").strip()
169
+ if m == "GET" and p == "/api/auth/hermes-dashboard-gate":
170
+ return AuthLevel.PUBLIC
171
+ if m == "GET" and p in GET_ALWAYS_PUBLIC_ROUTES:
172
+ return AuthLevel.PUBLIC
173
+ if p.startswith("/api/public/"):
174
+ return AuthLevel.PUBLIC
175
+ if m == "GET" and p in GET_SINGLE_USER_PUBLIC_ROUTES:
176
+ return AuthLevel.SINGLE_USER_GET
177
+ if m == "POST" and p in AUTH_FLOW_POST_ROUTES:
178
+ return AuthLevel.AUTH_FLOW
179
+ if p in INSTALL_ROUTE_METHODS.get(m, frozenset()):
180
+ return AuthLevel.INSTALL
181
+ if m == "GET" and p == "/api/install/stack":
182
+ return AuthLevel.INSTALL
183
+ if p.startswith("/api/"):
184
+ return AuthLevel.SESSION
185
+ return AuthLevel.PUBLIC
186
+
187
+
188
+ def deployment_allows_sessionless_data_get(
189
+ deployment_mode: str,
190
+ *,
191
+ dev_local_unsafe: bool = False,
192
+ ) -> bool:
193
+ return deployment_mode == "single_user_local" or dev_local_unsafe
194
+
195
+
196
+ def sessionless_get_allowed(
197
+ path: str,
198
+ *,
199
+ deployment_mode: str,
200
+ dev_local_unsafe: bool = False,
201
+ ) -> bool:
202
+ level = resolve_auth_level("GET", path)
203
+ if level == AuthLevel.PUBLIC:
204
+ return True
205
+ if level == AuthLevel.SINGLE_USER_GET:
206
+ return deployment_allows_sessionless_data_get(
207
+ deployment_mode, dev_local_unsafe=dev_local_unsafe,
208
+ )
209
+ return False
210
+
211
+
212
+ def authorize_request(
213
+ method: str,
214
+ path: str,
215
+ session_id: str,
216
+ *,
217
+ deployment_mode: str,
218
+ dev_local_unsafe: bool,
219
+ install_window_open: bool,
220
+ validate_session,
221
+ attach_user,
222
+ ) -> bool:
223
+ """Return True when the request may proceed to the handler."""
224
+ m = str(method or "GET").upper()
225
+ if dev_local_unsafe:
226
+ if session_id:
227
+ info = validate_session(session_id)
228
+ if info:
229
+ attach_user(info["user_id"])
230
+ return True
231
+ if m == "OPTIONS":
232
+ return True
233
+
234
+ level = resolve_auth_level(m, path)
235
+ sid = str(session_id or "").strip()
236
+
237
+ if level == AuthLevel.PUBLIC:
238
+ if sid:
239
+ info = validate_session(sid)
240
+ if info:
241
+ attach_user(info["user_id"])
242
+ return True
243
+
244
+ if level == AuthLevel.SINGLE_USER_GET:
245
+ if m != "GET":
246
+ return _require_session(sid, validate_session, attach_user)
247
+ if deployment_allows_sessionless_data_get(
248
+ deployment_mode, dev_local_unsafe=dev_local_unsafe,
249
+ ):
250
+ if sid:
251
+ info = validate_session(sid)
252
+ if info:
253
+ attach_user(info["user_id"])
254
+ return True
255
+ return _require_session(sid, validate_session, attach_user)
256
+
257
+ if level == AuthLevel.AUTH_FLOW:
258
+ return True
259
+
260
+ if level == AuthLevel.INSTALL:
261
+ if not install_window_open:
262
+ return False
263
+ if sid:
264
+ info = validate_session(sid)
265
+ if info:
266
+ attach_user(info["user_id"])
267
+ return True
268
+
269
+ if level == AuthLevel.SESSION:
270
+ return _require_session(sid, validate_session, attach_user)
271
+
272
+ return False
273
+
274
+
275
+ def lookup_route(method: str, path: str) -> Route | None:
276
+ return _ROUTES_BY_METHOD_PATH.get((str(method or "GET").upper(), str(path or "").strip()))
277
+
278
+
279
+ def dispatch_get(handler: Any, path: str, qs: dict[str, list[str]]) -> bool:
280
+ """Invoke a registered GET handler on *handler*; return True when dispatched."""
281
+ route = lookup_route("GET", path)
282
+ if not route:
283
+ return False
284
+ fn = getattr(handler, route.handler, None)
285
+ if not callable(fn):
286
+ raise RuntimeError(f"missing handler {route.handler!r} for GET {path}")
287
+ fn(qs)
288
+ return True
289
+
290
+
291
+ def dispatch_post(handler: Any, path: str, body: dict) -> bool:
292
+ """Invoke a registered POST handler on *handler*; return True when dispatched."""
293
+ route = lookup_route("POST", path)
294
+ if not route:
295
+ return False
296
+ fn = getattr(handler, route.handler, None)
297
+ if not callable(fn):
298
+ raise RuntimeError(f"missing handler {route.handler!r} for POST {path}")
299
+ fn(body)
300
+ return True
301
+
302
+
303
+ def _require_session(sid: str, validate_session, attach_user) -> bool:
304
+ if not sid:
305
+ return False
306
+ info = validate_session(sid)
307
+ if not info:
308
+ return False
309
+ attach_user(info["user_id"])
310
+ return True
311
+
312
+
313
+ def iter_registered_api_paths() -> frozenset[tuple[str, str]]:
314
+ """Exact (method, path) pairs declared in the registry (for audit tests)."""
315
+ out: set[tuple[str, str]] = set()
316
+ for p in GET_ALWAYS_PUBLIC_ROUTES:
317
+ out.add(("GET", p))
318
+ for p in GET_SINGLE_USER_PUBLIC_ROUTES:
319
+ out.add(("GET", p))
320
+ for p in AUTH_FLOW_POST_ROUTES:
321
+ out.add(("POST", p))
322
+ for m, paths in INSTALL_ROUTE_METHODS.items():
323
+ for p in paths:
324
+ out.add((m, p))
325
+ out.add(("GET", "/api/install/stack"))
326
+ out.add(("PATCH", "/api/install/stack"))
327
+ out.add(("GET", "/api/auth/hermes-dashboard-gate"))
328
+ for route in ROUTES:
329
+ out.add((route.method.upper(), route.path))
330
+ return frozenset(out)
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /** CI/harness API self-checks — same coverage as package.json typecheck. */
3
+ import { spawnSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const dir = path.dirname(fileURLToPath(import.meta.url));
8
+ const py = process.env.PYTHON || 'python';
9
+ const steps = [
10
+ ['-m', 'py_compile', 'server.py', 'zk_auth.py', 'email_sender.py', 'profile_config_yaml.py', 'route_registry.py'],
11
+ ['test_public_routes.py'],
12
+ ['test_route_registry.py'],
13
+ ['test_billing_provider.py'],
14
+ ['test_model_surface_consistency.py'],
15
+ ['test_profile_model_yaml.py'],
16
+ ];
17
+
18
+ for (const args of steps) {
19
+ const r = spawnSync(py, args, { cwd: dir, stdio: 'inherit' });
20
+ if (r.error) {
21
+ console.error(`typecheck spawn failed (${py}):`, r.error.message);
22
+ process.exit(1);
23
+ }
24
+ if (r.status !== 0) process.exit(r.status ?? 1);
25
+ }