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.
- package/README.md +1 -1
- package/docs/security.md +85 -0
- package/package.json +1 -1
- package/scripts/sync-canonical-to-package.mjs +8 -0
- package/scripts/verify-public-deploy.sh +59 -3
- package/workframe-api/openrouter_catalog.py +4 -4
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_config_yaml.py +126 -0
- package/workframe-api/route_registry.py +330 -0
- package/workframe-api/run-typecheck.mjs +25 -0
- package/workframe-api/server.py +1523 -1392
- package/workframe-api/test_model_surface_consistency.py +116 -0
- package/workframe-api/test_profile_model_yaml.py +99 -0
- package/workframe-api/test_public_routes.py +41 -0
- package/workframe-api/test_route_registry.py +109 -0
- package/workframe-api/time-bind-steps.py +75 -0
- package/workframe-api/time-perf.py +81 -0
- package/workframe-api/time-warm.py +107 -0
- package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-B0OFRGmJ.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DeBNltHS.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-BDhCHu7I.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-olcDYvtI.js} +1 -1
- package/workframe-ui/public/assets/channel-fNHbDpV4.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-D0obCBn_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DX93f-ZZ.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-BX9B5wUs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-C4DGc6RO.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-Crlja9Lf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-JYSm3szI.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-_KslxVEl.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-CpTO-jU8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-JGUkCmbI.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-BscBwGaC.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-5ZTZ0bpX.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-DxvitYbW.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-CsRdBqUB.js → chunk-LZXEDZCA-DMfdMUwz.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-Cayl0iV9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-_7dyBR5G.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O--xTpD0yi.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-CwxJQaeK.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-CCspENYv.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Dv8V6R60.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-Da48hFPT.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-dTihc7-3.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-D_Jqu699.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-Bt8PtEaZ.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-BtmPjlh0.js} +1 -1
- package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-Cz2turIT.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-BxMGCflX.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-ivyroIZt.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-B9llwShx.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-BFQHk4bw.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-ursegLbc.js} +1 -1
- package/workframe-ui/public/assets/index-8CuZDEIG.css +1 -0
- package/workframe-ui/public/assets/index-xS9lFekI.js +129 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-DQpifAsB.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-C16XP2Q7.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-CiJoz7rP.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-CVwEvvUL.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-CWQ6hX5i.js} +1 -1
- package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-CgQsmU35.js} +1 -1
- package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-Bxbc77dL.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-C_jMeF0a.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-4RKV9MZP.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-DZ7y7Sz0.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-B9h2I7Vq.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-B2mP5uHm.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-DyYKyKiP.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-CL_6nej-.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-CNKNDQ7S.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-eWU_mhFa.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-DrjcXvEQ.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-BHWR1xTJ.js} +1 -1
- package/workframe-ui/public/assets/{src-CY_By-r7.js → src-DfJB8kV1.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-BGJbSd3I.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-DkwoCjiZ.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-GxfhiqoW.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-DxrOsars.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-BsWhpUfr.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-CrFZWMHS.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DV8Xpf5I.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-DqMqnwdZ.js} +1 -1
- package/workframe-ui/public/index.html +4 -5
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
- package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
- package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
- package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
- 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
|
+
}
|