"""HTML data-injection + relative-URL token rewriting for served outputs. The token rewrite (`inject_token_into_relative_urls`) is a security boundary: iframe sub-resource fetches drop the parent's ?token= query, so the serve routes re-stamp it onto every relative href/src or they 401. Keep it wired to the serve routes.""" import base64 import json import logging import re from jsonschema import validate as schema_validate, ValidationError as SchemaValidationError logger = logging.getLogger(__name__) MODEL_MAP = { "sonnet": "claude-sonnet-4-20250514", "opus": "claude-opus-4-20250514", "haiku": "claude-haiku-4-5-20251001", } def resolve_model(short_name: str) -> str: return MODEL_MAP.get(short_name, short_name) def get_anthropic_client(api_model: str | None = None): """Create an AsyncAnthropic client using the API key from app settings. When `api_model` is provided and carries a 9Router prefix (cc/, cx/, gc/), the client is pointed at 9Router so non-Anthropic aux calls don't 400 on api.anthropic.com. Without an api_model we fall back to the default connection-mode-driven client. """ from backend.apps.settings.credentials import ( get_anthropic_client, get_anthropic_client_for_model, ) from backend.apps.settings.settings import load_settings settings = load_settings() if api_model: return get_anthropic_client_for_model(settings, api_model) return get_anthropic_client(settings) def validate_against_schema(data: dict, schema: dict) -> str | None: """Validate *data* against *schema*. Return an error string or None.""" try: schema_validate(instance=data, schema=schema) return None except SchemaValidationError as exc: path = " -> ".join(str(p) for p in exc.absolute_path) if exc.absolute_path else "(root)" return f"Schema validation failed at {path}: {exc.message}" def p_runtime_helpers_js() -> str: """OUTPUT_COMPUTE / OUTPUT_LLM only run for real on the published edge, where they are same-origin and carry NO credentials. In the App Builder preview we deliberately do NOT wire them to the authenticated backend: doing so would embed this install's token into the app's own JS (the exact exposure SECURITY.md item A is about). Preview defines readable stubs instead, the app degrades with a clear message rather than crashing or leaking a credential.""" return ( " window.OUTPUT_COMPUTE = async function () { throw new Error('OUTPUT_COMPUTE runs once this app is published.'); };\n" " window.OUTPUT_LLM = async function () { throw new Error('OUTPUT_LLM runs once this app is published.'); };\n" ) def build_data_injection(input_json: str, result_json: str, backend_url_json: str = "null", with_runtime: bool = False) -> str: """Build a " ) def inject_data_into_html(html: str, input_json: str = "{}", result_json: str = "null", backend_url_json: str = "null", with_runtime: bool = False) -> str: injection = build_data_injection(input_json, result_json, backend_url_json, with_runtime) if "" in html: return html.replace("", f"{injection}\n", 1) if " str: """Return the JSON-encoded backend URL for the given workspace, or "null" if no runtime is active. Cheap inline lookup so serve_workspace_file doesn't have to think about it.""" try: from backend.apps.outputs.runtime import manager as runtime_manager rt = runtime_manager.get(workspace_id) if rt and rt.running and rt.port: return json.dumps(f"http://127.0.0.1:{rt.port}") except Exception: logger.exception("backend url lookup failed for %s", workspace_id) return "null" # URL schemes / prefixes that must NOT have ?token= appended. These are either external (CDNs, mailto) or non-network references that the auth middleware never sees. Anything else is treated as a same-origin relative URL pointing at our /api/outputs/.../serve/ subtree, which DOES need the token. P_ABSOLUTE_URL_PREFIXES = ( "http://", "https://", "//", "data:", "blob:", "mailto:", "tel:", "javascript:", "about:", "#", ) P_HREF_SRC_ATTR_RE = re.compile( r"""(\s(?:href|src))\s*=\s*(["'])([^"']+)\2""", re.IGNORECASE, ) def inject_token_into_relative_urls(html: str, token: str) -> str: """Append `?token=` to every relative href/src in the served HTML. Browsers strip the parent iframe URL's query string before resolving relative `` / `