diff --git a/docs/slack-setup.md b/docs/slack-setup.md index dd69c69f..3d9de70b 100644 --- a/docs/slack-setup.md +++ b/docs/slack-setup.md @@ -35,9 +35,12 @@ 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, + 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/), [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..1067b470 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://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"] == \ + "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..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.com/api/slack/oauth/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 03ec9e8d..e79311ba 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://slack-oauth.messagebridge.app/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")