11"""Request-local workspace context for canonical permalink generation."""
22
3+ import re
34from contextlib import contextmanager
45from contextvars import ContextVar
56from dataclasses import dataclass
67from typing import Iterator
78
89WORKSPACE_SLUG_HEADER = "X-Basic-Memory-Workspace-Slug"
910WORKSPACE_TYPE_HEADER = "X-Basic-Memory-Workspace-Type"
11+ _WORKSPACE_SLUG_PATTERN = re .compile (r"^[a-z0-9_-]+$" )
12+ _WORKSPACE_TYPES = {"personal" , "organization" }
1013
1114
1215@dataclass (frozen = True )
@@ -32,6 +35,25 @@ def current_workspace_permalink_context() -> WorkspacePermalinkContext | None:
3235 return _workspace_permalink_context .get ()
3336
3437
38+ def validate_workspace_permalink_context_values (
39+ workspace_slug : str | None ,
40+ workspace_type : str | None ,
41+ ) -> None :
42+ """Validate workspace permalink metadata before it can affect stored permalinks."""
43+ if bool (workspace_slug ) != bool (workspace_type ):
44+ raise ValueError ("workspace_slug and workspace_type must be provided together" )
45+
46+ if not workspace_slug or not workspace_type :
47+ return
48+
49+ if _WORKSPACE_SLUG_PATTERN .fullmatch (workspace_slug ) is None :
50+ raise ValueError (f"{ WORKSPACE_SLUG_HEADER } must match [a-z0-9_-]+" )
51+
52+ if workspace_type not in _WORKSPACE_TYPES :
53+ allowed = ", " .join (sorted (_WORKSPACE_TYPES ))
54+ raise ValueError (f"{ WORKSPACE_TYPE_HEADER } must be one of: { allowed } " )
55+
56+
3557@contextmanager
3658def workspace_permalink_context (
3759 workspace_slug : str | None ,
@@ -42,8 +64,7 @@ def workspace_permalink_context(
4264 Cloud can populate this per request without storing workspace metadata in
4365 local project config. The slug/type pair is all permalink generation needs.
4466 """
45- if bool (workspace_slug ) != bool (workspace_type ):
46- raise ValueError ("workspace_slug and workspace_type must be provided together" )
67+ validate_workspace_permalink_context_values (workspace_slug , workspace_type )
4768
4869 if not workspace_slug or not workspace_type :
4970 yield
0 commit comments