From 575f1a4eef3f7e50ddf7ab26ba7012f05b9550ba Mon Sep 17 00:00:00 2001 From: Jeff Huber Date: Fri, 18 Sep 2026 02:14:43 -0700 Subject: [PATCH 1/2] Slack: route OAuth through the privacy relay --- docs/slack-setup.md | 8 +++++--- scripts/rehearse_v150.py | 6 ++++++ src/code_mower/templates/slack/hosted-app-manifest.json | 2 +- tests/test_slack_setup.py | 7 +++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/slack-setup.md b/docs/slack-setup.md index dd69c69f..b1754383 100644 --- a/docs/slack-setup.md +++ b/docs/slack-setup.md @@ -35,9 +35,11 @@ public channels and rich Slack UX are deferred to v1.5.1. 2. An authorized administrator imports the generated manifest into the private Slack app. Preserve exactly the bot `commands` scope, no user scopes or Events API subscriptions, no organization-wide install, and token rotation. The - hosted command/interactivity/OAuth routes are fixed in the manifest; do not - substitute previews, localhost or private URLs. Verify the installed settings - match: a generated file does not prove installation. See Slack's official + hosted routes are fixed in the manifest: command and interactivity requests go + to the application, while the OAuth redirect goes only through the dedicated + query-scrubbing relay before a query-free browser handoff to the application. + Do not substitute previews, localhost or private URLs. Verify the installed + settings match: a generated file does not prove installation. See Slack's official [manifest](https://docs.slack.dev/reference/app-manifest/), [OAuth](https://docs.slack.dev/authentication/installing-with-oauth/) and [token rotation](https://docs.slack.dev/authentication/using-token-rotation/) diff --git a/scripts/rehearse_v150.py b/scripts/rehearse_v150.py index 6f309641..f653e822 100644 --- a/scripts/rehearse_v150.py +++ b/scripts/rehearse_v150.py @@ -231,6 +231,12 @@ def cli(python, *args, expected=0): assert output.stat().st_mode & 0o777 == 0o600 hosted_manifest = json.loads(output.read_text()) assert hosted_manifest["oauth_config"]["scopes"]["bot"] == ["commands"] + assert hosted_manifest["oauth_config"]["redirect_urls"] == [ + "https://codemower-slack-oauth-ingress.jhuber.workers.dev/callback"] + assert hosted_manifest["features"]["slash_commands"][0]["url"] == \ + "https://codemower.com/api/slack/commands" + assert hosted_manifest["settings"]["interactivity"]["request_url"] == \ + "https://codemower.com/api/slack/interactions" cli(py, "slack", "setup", "--manifest", output, "--yes", expected=1) checks.append("explicit_slack_setup_exclusive_private_manifest") report = json.loads(cli(py, "slack", "doctor", "--json", expected=1)) diff --git a/src/code_mower/templates/slack/hosted-app-manifest.json b/src/code_mower/templates/slack/hosted-app-manifest.json index 3810b07d..72406698 100644 --- a/src/code_mower/templates/slack/hosted-app-manifest.json +++ b/src/code_mower/templates/slack/hosted-app-manifest.json @@ -15,7 +15,7 @@ }] }, "oauth_config": { - "redirect_urls": ["https://codemower.com/api/slack/oauth/callback"], + "redirect_urls": ["https://codemower-slack-oauth-ingress.jhuber.workers.dev/callback"], "scopes": {"bot": ["commands"]} }, "settings": { diff --git a/tests/test_slack_setup.py b/tests/test_slack_setup.py index 03ec9e8d..8621b8ae 100644 --- a/tests/test_slack_setup.py +++ b/tests/test_slack_setup.py @@ -213,6 +213,13 @@ def test_scripted_opt_in_exports_only_hosted_manifest_and_never_overwrites(self) self.assertEqual(self.call([*args, "--yes"])[0], 0) manifest = json.loads(path.read_bytes()) self.assertEqual(manifest["oauth_config"]["scopes"], {"bot": ["commands"]}) + self.assertEqual(manifest["oauth_config"]["redirect_urls"], [ + "https://codemower-slack-oauth-ingress.jhuber.workers.dev/callback" + ]) + self.assertEqual(manifest["features"]["slash_commands"][0]["url"], + "https://codemower.com/api/slack/commands") + self.assertEqual(manifest["settings"]["interactivity"]["request_url"], + "https://codemower.com/api/slack/interactions") self.assertTrue(manifest["settings"]["token_rotation_enabled"]) self.assertNotIn("event_subscriptions", manifest["settings"]) self.assertEqual(manifest["features"]["slash_commands"][0]["command"], "/codemower") From 4a48986d530efe40c508a1825197f5956054fa5a Mon Sep 17 00:00:00 2001 From: Jeff Huber Date: Fri, 18 Sep 2026 02:18:09 -0700 Subject: [PATCH 2/2] Use an owner-controlled OAuth relay domain --- docs/slack-setup.md | 5 +++-- scripts/rehearse_v150.py | 2 +- src/code_mower/templates/slack/hosted-app-manifest.json | 2 +- tests/test_slack_setup.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/slack-setup.md b/docs/slack-setup.md index b1754383..3d9de70b 100644 --- a/docs/slack-setup.md +++ b/docs/slack-setup.md @@ -36,8 +36,9 @@ public channels and rich Slack UX are deferred to v1.5.1. Slack app. Preserve exactly the bot `commands` scope, no user scopes or Events API subscriptions, no organization-wide install, and token rotation. The hosted routes are fixed in the manifest: command and interactivity requests go - to the application, while the OAuth redirect goes only through the dedicated - query-scrubbing relay before a query-free browser handoff to the application. + to the application, while the OAuth redirect goes only through the dedicated, + owner-controlled query-scrubbing relay before a query-free browser handoff to + the application. Do not substitute previews, localhost or private URLs. Verify the installed settings match: a generated file does not prove installation. See Slack's official [manifest](https://docs.slack.dev/reference/app-manifest/), diff --git a/scripts/rehearse_v150.py b/scripts/rehearse_v150.py index f653e822..1067b470 100644 --- a/scripts/rehearse_v150.py +++ b/scripts/rehearse_v150.py @@ -232,7 +232,7 @@ def cli(python, *args, expected=0): hosted_manifest = json.loads(output.read_text()) assert hosted_manifest["oauth_config"]["scopes"]["bot"] == ["commands"] assert hosted_manifest["oauth_config"]["redirect_urls"] == [ - "https://codemower-slack-oauth-ingress.jhuber.workers.dev/callback"] + "https://slack-oauth.messagebridge.app/callback"] assert hosted_manifest["features"]["slash_commands"][0]["url"] == \ "https://codemower.com/api/slack/commands" assert hosted_manifest["settings"]["interactivity"]["request_url"] == \ diff --git a/src/code_mower/templates/slack/hosted-app-manifest.json b/src/code_mower/templates/slack/hosted-app-manifest.json index 72406698..0e71e538 100644 --- a/src/code_mower/templates/slack/hosted-app-manifest.json +++ b/src/code_mower/templates/slack/hosted-app-manifest.json @@ -15,7 +15,7 @@ }] }, "oauth_config": { - "redirect_urls": ["https://codemower-slack-oauth-ingress.jhuber.workers.dev/callback"], + "redirect_urls": ["https://slack-oauth.messagebridge.app/callback"], "scopes": {"bot": ["commands"]} }, "settings": { diff --git a/tests/test_slack_setup.py b/tests/test_slack_setup.py index 8621b8ae..e79311ba 100644 --- a/tests/test_slack_setup.py +++ b/tests/test_slack_setup.py @@ -214,7 +214,7 @@ def test_scripted_opt_in_exports_only_hosted_manifest_and_never_overwrites(self) manifest = json.loads(path.read_bytes()) self.assertEqual(manifest["oauth_config"]["scopes"], {"bot": ["commands"]}) self.assertEqual(manifest["oauth_config"]["redirect_urls"], [ - "https://codemower-slack-oauth-ingress.jhuber.workers.dev/callback" + "https://slack-oauth.messagebridge.app/callback" ]) self.assertEqual(manifest["features"]["slash_commands"][0]["url"], "https://codemower.com/api/slack/commands")