From 66f3b7ffb4216913085dd4d01bd8563399d85cf0 Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Sun, 1 Mar 2026 22:19:12 -0500 Subject: [PATCH 1/6] feat: add normal hackatime token revocations, multi-token type matching --- README.md | 2 +- app/models/token_types.rb | 40 +++++++++-------- app/models/token_types/hackatime_key.rb | 57 +++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 app/models/token_types/hackatime_key.rb diff --git a/README.md b/README.md index d6be360..8abbee0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # the revoker - + API token amnesty box - revoke leaked tokens: paste one in, it gets detected and revoked automatically. the token owner gets notified via slack DM and email. diff --git a/app/models/token_types.rb b/app/models/token_types.rb index ff015e0..bd6f73f 100644 --- a/app/models/token_types.rb +++ b/app/models/token_types.rb @@ -1,3 +1,18 @@ +require_relative "token_types/base" +require_relative "token_types/airtable_pat" +require_relative "token_types/flavortown_api_key" +require_relative "token_types/hack_club_ai_api_key" +require_relative "token_types/hack_club_search_api_key" +require_relative "token_types/hackatime_admin_key" +require_relative "token_types/hack_club_cdn_api_key" +require_relative "token_types/hcb_oauth" +require_relative "token_types/slack_xoxb" +require_relative "token_types/slack_xoxp" +require_relative "token_types/slack_xoxc" +require_relative "token_types/slack_xoxd" +require_relative "token_types/theseus_api_key" +require_relative "token_types/hackatime_key" + module TokenTypes # Registry of all token types ALL = [ @@ -13,29 +28,18 @@ module TokenTypes SlackXoxc, SlackXoxd, TheseusAPIKey, - TheseusPublicAPIKey + TheseusPublicAPIKey, + HackatimeToken ].freeze def self.find(value) - ALL.find { |token_type| token_type.matches?(value) } + ALL.select { |token_type| token_type.matches?(value) } end def self.detect_type(value) - token_type = find(value) - token_type&.display_name || "Unknown" + token_types = find(value) + return "Unknown" if token_types.empty? + + token_types.map(&:display_name).join(", ") end end - -require_relative "token_types/base" -require_relative "token_types/airtable_pat" -require_relative "token_types/flavortown_api_key" -require_relative "token_types/hack_club_ai_api_key" -require_relative "token_types/hack_club_search_api_key" -require_relative "token_types/hackatime_admin_key" -require_relative "token_types/hack_club_cdn_api_key" -require_relative "token_types/hcb_oauth" -require_relative "token_types/slack_xoxb" -require_relative "token_types/slack_xoxp" -require_relative "token_types/slack_xoxc" -require_relative "token_types/slack_xoxd" -require_relative "token_types/theseus_api_key" diff --git a/app/models/token_types/hackatime_key.rb b/app/models/token_types/hackatime_key.rb new file mode 100644 index 0000000..dcb6d85 --- /dev/null +++ b/app/models/token_types/hackatime_key.rb @@ -0,0 +1,57 @@ +module TokenTypes + class HackatimeToken < Base + self.regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i + self.name = "Hackatime v2 API key" + + def self.revoke(token, **_kwargs) + logger_prefix = "HackatimeToken" + Rails.logger.info("#{logger_prefix}: Starting revocation for token") + + hackatime_url = ENV["HACKATIME_API_URL"] || "http://localhost:3000" + auth_token = ENV["HACKATIME_AUTH_TOKEN"] + + unless auth_token + Rails.logger.error("#{logger_prefix}: HACKATIME_AUTH_TOKEN not configured") + Sentry.capture_message("HACKATIME_AUTH_TOKEN not configured", level: :error) + return { success: false } + end + + begin + connection = Faraday.new(url: hackatime_url) do |faraday| + faraday.request :json + faraday.response :json + end + + Rails.logger.info("#{logger_prefix}: Making POST request to #{hackatime_url}/api/internal/revoke_normal") + + response = connection.post("/api/internal/revoke_normal", {}, { + "Authorization" => "Bearer #{auth_token}" + }) do |req| + req.params["token"] = token + end + + body = response.body + Rails.logger.info("#{logger_prefix}: Response status=#{response.status}, body=#{body.inspect}") + + if response.success? && body["success"] + owner_email = body["owner_email"] + key_name = body["key_name"] + status = body["status"] + Rails.logger.info("#{logger_prefix}: Token revoked, owner_email=#{owner_email}, key_name=#{key_name}, status=#{status}") + result = { success: true, owner_email: owner_email } + result[:key_name] = key_name if key_name + result[:status] = status.to_sym if status + result + else + Rails.logger.warn("#{logger_prefix}: API request failed or returned success=false") + { success: false } + end + rescue StandardError => e + Rails.logger.error("#{logger_prefix}: Exception during revocation - #{e.class}: #{e.message}") + Rails.logger.error(e.backtrace.join("\n")) + Sentry.capture_exception(e) + { success: false } + end + end + end +end \ No newline at end of file From be81c6fc84175f2fc6d51680515c83a0a70f33e3 Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Sun, 1 Mar 2026 22:24:48 -0500 Subject: [PATCH 2/6] chore: add required newline to token_types.rb (thanks linter <3) --- app/models/token_types/hackatime_key.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/token_types/hackatime_key.rb b/app/models/token_types/hackatime_key.rb index dcb6d85..c25f22b 100644 --- a/app/models/token_types/hackatime_key.rb +++ b/app/models/token_types/hackatime_key.rb @@ -54,4 +54,4 @@ def self.revoke(token, **_kwargs) end end end -end \ No newline at end of file +end From f73679ed5845c137127384dc2d91dbc6223710c6 Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Sun, 1 Mar 2026 22:34:03 -0500 Subject: [PATCH 3/6] chore: make test runner not hate me (standardize naming conventions) --- app/models/token_types.rb | 2 +- app/models/token_types/{hackatime_key.rb => hackatime_token.rb} | 0 test/controllers/revocations_controller_test.rb | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/models/token_types/{hackatime_key.rb => hackatime_token.rb} (100%) diff --git a/app/models/token_types.rb b/app/models/token_types.rb index bd6f73f..21d4743 100644 --- a/app/models/token_types.rb +++ b/app/models/token_types.rb @@ -11,7 +11,7 @@ require_relative "token_types/slack_xoxc" require_relative "token_types/slack_xoxd" require_relative "token_types/theseus_api_key" -require_relative "token_types/hackatime_key" +require_relative "token_types/hackatime_token" module TokenTypes # Registry of all token types diff --git a/app/models/token_types/hackatime_key.rb b/app/models/token_types/hackatime_token.rb similarity index 100% rename from app/models/token_types/hackatime_key.rb rename to app/models/token_types/hackatime_token.rb diff --git a/test/controllers/revocations_controller_test.rb b/test/controllers/revocations_controller_test.rb index 8b7292c..366d2e5 100644 --- a/test/controllers/revocations_controller_test.rb +++ b/test/controllers/revocations_controller_test.rb @@ -2,7 +2,7 @@ class RevocationsControllerTest < ActionDispatch::IntegrationTest test "should get new" do - get revocations_new_url + get new_revocation_url assert_response :success end end From 18f8c895190316e2cf743466abae5e3cd7816196 Mon Sep 17 00:00:00 2001 From: Mat Manna <91392083+matmanna@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:31:48 -0500 Subject: [PATCH 4/6] docs: update supported tokens in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8abbee0..4a10ce7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ revoke leaked tokens: paste one in, it gets detected and revoked automatically. - hack club AI API keys - hack club search API keys - hackatime admin keys +- hackatime api keys - HCB OAuth tokens - slack tokens (xoxb, xoxp, xoxc, xoxd) - theseus API keys From 9d35c79633056e9153d6a908b2704cb1755896a5 Mon Sep 17 00:00:00 2001 From: Mat Manna <91392083+matmanna@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:55:24 +0000 Subject: [PATCH 5/6] fix: use the same hackatime endpoint for both revocation types --- app/models/token_types/hackatime_token.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/token_types/hackatime_token.rb b/app/models/token_types/hackatime_token.rb index c25f22b..69b6458 100644 --- a/app/models/token_types/hackatime_token.rb +++ b/app/models/token_types/hackatime_token.rb @@ -22,9 +22,9 @@ def self.revoke(token, **_kwargs) faraday.response :json end - Rails.logger.info("#{logger_prefix}: Making POST request to #{hackatime_url}/api/internal/revoke_normal") + Rails.logger.info("#{logger_prefix}: Making POST request to #{hackatime_url}/api/internal/revoke") - response = connection.post("/api/internal/revoke_normal", {}, { + response = connection.post("/api/internal/revoke", {}, { "Authorization" => "Bearer #{auth_token}" }) do |req| req.params["token"] = token From 2447929b6e2875af6b3ed8e931d8d5153cb6f38f Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Wed, 1 Apr 2026 21:35:06 -0400 Subject: [PATCH 6/6] unfix: test error --- test/controllers/revocations_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/revocations_controller_test.rb b/test/controllers/revocations_controller_test.rb index 366d2e5..8b7292c 100644 --- a/test/controllers/revocations_controller_test.rb +++ b/test/controllers/revocations_controller_test.rb @@ -2,7 +2,7 @@ class RevocationsControllerTest < ActionDispatch::IntegrationTest test "should get new" do - get new_revocation_url + get revocations_new_url assert_response :success end end