{"id":50771723,"url":"https://github.com/flucksite/lucky_honeypot","last_synced_at":"2026-06-11T19:31:09.861Z","repository":{"id":325509862,"uuid":"1101459058","full_name":"flucksite/lucky_honeypot","owner":"flucksite","description":" 🍯 Simple invisible captcha spam protection for Lucky Framework apps.","archived":false,"fork":false,"pushed_at":"2026-04-26T16:57:39.000Z","size":92,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-26T18:25:21.594Z","etag":null,"topics":["bot-protection","captcha","crystal","crystal-lang","invisible-captcha","lucky","luckyframework","security"],"latest_commit_sha":null,"homepage":"https://codeberg.org/fluck/lucky_honeypot","language":"Crystal","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flucksite.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-21T17:47:56.000Z","updated_at":"2026-04-26T16:57:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/flucksite/lucky_honeypot","commit_stats":null,"previous_names":["flucksite/lucky_honeypot"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/flucksite/lucky_honeypot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flucksite%2Flucky_honeypot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flucksite%2Flucky_honeypot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flucksite%2Flucky_honeypot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flucksite%2Flucky_honeypot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flucksite","download_url":"https://codeload.github.com/flucksite/lucky_honeypot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flucksite%2Flucky_honeypot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34215253,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot-protection","captcha","crystal","crystal-lang","invisible-captcha","lucky","luckyframework","security"],"created_at":"2026-06-11T19:31:08.908Z","updated_at":"2026-06-11T19:31:09.856Z","avatar_url":"https://github.com/flucksite.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# lucky_honeypot\n\n[![CI](https://codeberg.org/fluck/lucky_honeypot/actions/workflows/test.yml/badge.svg)](https://codeberg.org/fluck/lucky_honeypot/actions?workflow=test.yml)\n[![Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcodeberg.org%2Fapi%2Fv1%2Frepos%2Ffluck%2Flucky_honeypot%2Ftags\u0026query=%24%5B0%5D.name\u0026label=version)](https://codeberg.org/fluck/lucky_honeypot/tags)\n\nSimple invisible captcha spam protection for Lucky Framework apps.\n\n\u003e [!Note]\n\u003e The original repository is hosted at\n\u003e [https://codeberg.org/fluck/lucky_honeypot](https://codeberg.org/fluck/lucky_honeypot).\n\n## How it works\n\nThis shard uses three techniques to catch spambots:\n\n1. **Invisible fields**. Bots fill out every field, including ones hidden with CSS.\n2. **Timing checks**. Bots submit forms instantly, humans need more time.\n3. **Input signals**. Bots don't tend to trigger mouse, touch, scroll, keyboard, or focus events.\n\nWhen either of the two first checks fail, the submission is quietly rejected.\nThe bot thinks it succeeded and moves on. The third one can be used to reject\nsubmissions at a certain _human rating_ threshold, or to flag entries that may\nbe suspicious.\n\n## Installation\n\n1. Add the dependency to your `shard.yml`:\n\n   ```yaml\n   dependencies:\n     lucky_honeypot:\n       codeberg: fluck/lucky_honeypot\n   ```\n\n2. Run `shards install`\n\n## Usage\n\nRequire the shard in your `src/shards.cr`:\n\n```crystal\nrequire \"lucky_honeypot\"\n```\n\nAdd the honeypot to the form you would like to protect:\n\n```crystal\nclass SignUps::NewPage \u003c AuthLayout\n  include LuckyHoneypot::Tag\n\n  def content\n    form_for SignUps::Create do\n      # ...\n      honeypot_input \"user:website\"\n      # ...\n    end\n  end\nend\n```\n\n\u003e [!TIP]\n\u003e The name of the honeypot input can be anything, but it's best to keep it in\n\u003e line with the rest of the fields in your form, so it looks believable.\n\nFinally, configure the honeypot in the receiving action:\n\n```crystal\nclass SignUps::Create \u003c BrowserAction\n  include LuckyHoneypot::Pipe\n\n  honeypot \"user:website\"\n\n  post \"/sign_up\" do\n    # ...\n  end\nend\n```\n\nThat's it! Your form is now protected.\n\n### Configuring the input\n\nThe `honeypot_input` is good to go out of the box, but there are some things to\nconsider. By default, it is rendered with a `style` attribute to make it\ninaccessible for humans, so they don't accidentally fill it out. By passing a\nclass attribute, it is assumed that you're using your own CSS class to hide the\ninput:\n\n```crystal\nhoneypot_input \"user:website\", class: \"visually-hidden\"\n```\n\n\u003e [!IMPORTANT]\n\u003e When a `class` attribute is passed, the default `style` attribute won't be\n\u003e rendered. However, you can pass your own `style` attribute as well.\n\nAside from the special `class` attribute, you can pass any other attribute as\nwell:\n\n```crystal\nhoneypot_input \"user:website\", data_purpose: \"not for humans\"\n```\n\n\u003e [!NOTE]\n\u003e The `honeypot_input` macro method will also set a timestamp in the session to\n\u003e verify submission timing. Consider this when you add your own field instead\n\u003e of using this macro.\n\n### Configuration options\n\n```crystal\nHabitat.configure do |settings|\n  # Default required delay between page load and form submission.\n  settings.default_delay = 2.seconds\n\n  # Disables the submission delay entirely; useful in test environments.\n  settings.disable_delay = false\n\n  # Default name for the signals input field\n  settings.signals_input_name = \"honeypot_signals\"\nend\n```\n\n### Configuring the pipe\n\nThe `honeypot` macro does two things:\n\n1. Ensure the declared honeypot field is not filled\n2. Ensure the form was not submitted too quickly\n\nThe default timing for the form submission is 2 seconds, but that can be\nconfigured:\n\n```crystal\nhoneypot \"user:website\", wait: 5.seconds\n# or\nhoneypot \"user:website\", wait: 1_500.milliseconds\n```\n\n\u003e [!TIP]\n\u003e The ideal timing will depend on the length of your form. Do some testing to\n\u003e see how fast you can fill out the form, and use that as the baseline.\n\nIf a honeypot is filled or submitted too quickly, a `head 204 (No Content)` will\nbe returned. This is common behaviour for honeypots. The bot will assume the\nsubmission was successful and move on to its next target. If you want to\nhandle it differently, can can pass a block with the desired response:\n\n```crystal\nhoneypot \"user:website\" do\n  flash.info = \"Moving on...\"\n  redirect to: Home::Index, status: HTTP::Status::SEE_OTHER\nend\n```\n\nFinally, you can also add multiple honeypots, each with their own timing and\nHTTP handling:\n\n```crystal\nhoneypot \"user:website\", wait: 5.seconds\nhoneypot \"note\" do\n  html Bot::IndexPage\nend\n```\n\n## Detecting input signals\n\nThis shard comes with simple input signals detection built-in. It monitors\nmouse movements, touch gestures, scroll triggers, keyboard input, and focus\nevents. If any of those are detected, it adds to the likeliness of human\ninteraction.\n\nTo track the input signals, add the `honeypot_signals` tag to your form:\n\n```crystal\nhoneypot_signals\n```\n\nSimilar to the `honeypot_input` tag, it accepts additional attributes:\n\n```crystal\nhoneypot_signals data_some: \"value\"\n```\n\nThe signals tag only tracks input signals and stores the result in a hidden\nfield that is submitted with the form. It is up to you what to do with the\ninformation. You could for example use it to flag a record in case of a\nsuspicious submission:\n\n```crystal\nif LuckyHoneypot::Signals.human_rating(params.get(\"honeypot_signals\")) \u003c 0.2\n  # Do your thing\nend\n```\n\nAnd if you want more information about which inputs were triggered:\n\n```crystal\nsignals = LuckyHoneypot::Signals.from_json(params.get(\"honeypot_signals\"))\nsignals.human_rating  # a value between 0 (bot) and 1 (human)\nsignals.mouse?        # if true, the mouse was moved\nsignals.touch?        # if true, a touch gesture was detected\nsignals.scroll?       # if true, a scroll was triggered\nsignals.keyboard?     # if true, keyboard input was detected\nsignals.focus?        # if true, input focus was triggered\n```\n\n\u003e [!NOTE]\n\u003e The human rating is the fraction of the five signals (mouse, touch, scroll,\n\u003e keyboard, focus) that fired, so each one contributes `0.2`. A score of `0`\n\u003e is almost certainly a dumb bot, while `0.2` could be a sophisticated bot\n\u003e triggering a single signal, though a human filling out a short form at the\n\u003e top of the page may also land there.\n\u003e\n\u003e `0.4` is a reasonable rejection threshold: it still catches bots that fake\n\u003e one or two signals, but avoids false positives for autofill and password\n\u003e manager submissions, which often only trigger focus plus mouse or touch.\n\u003e Use `0.6` as a \"flag as suspicious\" threshold rather than a hard reject,\n\u003e since legitimate autofill users will frequently fall below it.\n\n## Security considerations\n\nThis shard provides basic bot protection, but it should not be your only line of\ndefense. Here are few important points to consider:\n\n- It's not foolproof and sophisticated bots can bypass honeypots\n- Combine this with Lucky's built-in rate limiting feature\n- For high-value forms, consider adding CAPTCHA or email verification\n- The submission timestamp is stored in the session. If sessions are\n  compromised, an attacker could manipulate timing checks. Make sure your\n  session store is secure and uses signed cookies (Lucky's default)\n- The timing check compares wall-clock timestamps, which makes it resilient to\n  timing attacks since the check is a simple threshold comparison\n- This shard works alongside Lucky's built-in CSRF protection. The honeypot\n  fields are regular form inputs and do not interfere with CSRF tokens\n\nFor most use cases (contact forms, newsletter signups, etc.), this shard\nprovides excellent protection with zero user friction. By adding a honeypot,\nyou'll catch between 60% and 90% of all automated form submissions.\n\nIf you want protection from more sophisticated bots, have a look at the\n[Prosopo shard](https://codeberg.org/fluck/prosopo) or the [hCaptcha\nshard](https://codeberg.org/fluck/hcaptcha).\n\n## Contributing\n\nWe use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)\nfor our commit messages, so please adhere to that pattern.\n\n1. Fork it (\u003chttps://codeberg.org/fluck/lucky_honeypot/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'feat: add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## Contributors\n\n- [Wout](https://codeberg.org/w0u7) - creator and maintainer\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflucksite%2Flucky_honeypot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflucksite%2Flucky_honeypot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflucksite%2Flucky_honeypot/lists"}