diff --git a/scripts/odysseus-webhook b/scripts/odysseus-webhook index f3f162f90..fb7bc6de5 100755 --- a/scripts/odysseus-webhook +++ b/scripts/odysseus-webhook @@ -2,7 +2,7 @@ """odysseus-webhook — shell wrapper for scheduled-task webhook tokens. Tasks in the scheduled-task system can carry a `webhook_token`. Any -HTTP POST to `/api/webhook/` fires the task. This CLI lists, +HTTP POST to `/api/tasks//webhook/` fires the task. This CLI lists, rotates, and revokes those tokens. odysseus-webhook list # tasks that have a token @@ -21,6 +21,7 @@ quiet_logs() import argparse, json, logging, os, secrets, sys from pathlib import Path +from urllib.parse import quote try: from core.database import SessionLocal, ScheduledTask @@ -53,6 +54,14 @@ def _summary(t: "ScheduledTask", reveal: bool = False) -> dict: } +def _task_webhook_url(base: str, task_id: str, token: str) -> str: + """Build the live task-route URL without leaking ids into path syntax.""" + root = (base or "http://localhost:7000").rstrip("/") + task_part = quote(str(task_id), safe="") + token_part = quote(str(token), safe="") + return f"{root}/api/tasks/{task_part}/webhook/{token_part}" + + def cmd_list(args): db = SessionLocal() try: @@ -109,8 +118,7 @@ def cmd_url(args): fail(f"no task with id {args.id!r}") if not t.webhook_token: fail(f"task {args.id!r} has no webhook token (rotate one first)") - base = (args.base or "http://localhost:7000").rstrip("/") - url = f"{base}/api/webhook/{t.webhook_token}" + url = _task_webhook_url(args.base, t.id, t.webhook_token) emit({ "task_id": t.id, "name": t.name, diff --git a/tests/cli/test_webhook_cli_mask.py b/tests/cli/test_webhook_cli_mask.py index d98e5c906..17a283970 100644 --- a/tests/cli/test_webhook_cli_mask.py +++ b/tests/cli/test_webhook_cli_mask.py @@ -10,3 +10,28 @@ def test_mask_token_handles_short_values(monkeypatch): assert cli._mask_token("short") == "***" assert cli._mask_token("abcdef1234567890") == "abcdef…7890" assert cli._mask_token("short", reveal=True) == "short" + + +def test_task_webhook_url_matches_live_route_and_escapes_path_parts(monkeypatch): + make_core_db_stub(monkeypatch, models=["ScheduledTask"]) + cli = load_script("odysseus-webhook") + + url = cli._task_webhook_url( + "https://ody.example/", + "task/with space", + "token/with space", + ) + + assert url == ( + "https://ody.example/api/tasks/task%2Fwith%20space/" + "webhook/token%2Fwith%20space" + ) + + +def test_default_task_webhook_url_uses_current_task_prefix(monkeypatch): + make_core_db_stub(monkeypatch, models=["ScheduledTask"]) + cli = load_script("odysseus-webhook") + + assert cli._task_webhook_url(None, "task-1", "secret") == ( + "http://localhost:7000/api/tasks/task-1/webhook/secret" + )