{"_id":"@awgmi/contact-guard","_rev":"3-5f5914d4e7a94dfcce5b6cce4dd95ecc","name":"@awgmi/contact-guard","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@awgmi/contact-guard","version":"1.0.0","keywords":["contact-form","spam-protection","honeypot","rate-limiting","openrouter","resend"],"license":"MIT","_id":"@awgmi/contact-guard@1.0.0","maintainers":[{"name":"supakaity","email":"kaity@atikayda.au"}],"homepage":"https://github.com/AWGMI/contact-guard#readme","bugs":{"url":"https://github.com/AWGMI/contact-guard/issues"},"dist":{"shasum":"ba713e17813191d5f3ca1b2141de73c6bcc38601","tarball":"https://registry.npmjs.org/@awgmi/contact-guard/-/contact-guard-1.0.0.tgz","fileCount":13,"integrity":"sha512-2joCTjO/kNRIZ32vcyjthZmfnLGrkQg9xAA1uwO78Y3bAylCL+234cmPQw1M7TbYBQUsivK54bbnByW7cvJzJA==","signatures":[{"sig":"MEYCIQC2j92oRSDT71YXYN5ICWYCetIZ1yg8qWhguvq0BwkNiwIhAPD/gH9nmv7vkLsj8gRyBmIyDV/YGF740uPqUFovy+RL","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":16592},"type":"module","engines":{"node":">=20"},"exports":"./index.js","gitHead":"3b574ba3aa048bc05619090d15be602844a8cef8","_npmUser":{"name":"supakaity","email":"kaity@atikayda.au"},"repository":{"url":"git+https://github.com/AWGMI/contact-guard.git","type":"git"},"_npmVersion":"10.9.4","description":"Zero-dependency spam classification for contact forms. OpenRouter + honeypot defences with fail-open design.","directories":{},"_nodeVersion":"22.22.0","_hasShrinkwrap":false,"peerDependencies":{"hono":">=4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/contact-guard_1.0.0_1772452218317_0.3625514593981587","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@awgmi/contact-guard","version":"1.0.1","keywords":["contact-form","spam-protection","honeypot","rate-limiting","openrouter","resend"],"license":"MIT","_id":"@awgmi/contact-guard@1.0.1","maintainers":[{"name":"supakaity","email":"kaity@atikayda.au"}],"homepage":"https://github.com/AWGMI/contact-guard#readme","bugs":{"url":"https://github.com/AWGMI/contact-guard/issues"},"dist":{"shasum":"ed9c7e82e8df48e79fd2d3dec04cad287c1f4d49","tarball":"https://registry.npmjs.org/@awgmi/contact-guard/-/contact-guard-1.0.1.tgz","fileCount":13,"integrity":"sha512-bCArkBf29yK9RXmxTEDTnaM9TGx0Rr/2qGkRFZVJc6NGTXVuNNzkjyevTSXurPmfJBVDvuvRQt33f0EF9KoW0A==","signatures":[{"sig":"MEQCIGy8uucPXNcoNdWg9tDlFONVJmCNxkPpngSj6kePReKIAiAOgKiHG2rVhLnpPOqGFAvbdtdMzUag/gk7huaMSfecJQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@awgmi%2fcontact-guard@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":16562},"type":"module","_from":"file:awgmi-contact-guard-1.0.1.tgz","engines":{"node":">=20"},"exports":"./index.js","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:75a375a1-4aa4-4d09-9447-6f516152beb2"}},"_resolved":"/tmp/3ea1cf5f27ffea525fca0eed15147b3a/awgmi-contact-guard-1.0.1.tgz","_integrity":"sha512-bCArkBf29yK9RXmxTEDTnaM9TGx0Rr/2qGkRFZVJc6NGTXVuNNzkjyevTSXurPmfJBVDvuvRQt33f0EF9KoW0A==","repository":{"url":"git+https://github.com/AWGMI/contact-guard.git","type":"git"},"_npmVersion":"11.8.0","description":"Zero-dependency spam classification for contact forms. OpenRouter + honeypot defences with fail-open design.","directories":{},"_nodeVersion":"24.13.1","_hasShrinkwrap":false,"peerDependencies":{"hono":">=4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/contact-guard_1.0.1_1772454243498_0.41980970555735353","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@awgmi/contact-guard","version":"1.0.2","description":"Zero-dependency spam classification for contact forms. OpenRouter + honeypot defences with fail-open design.","type":"module","exports":"./index.js","license":"MIT","engines":{"node":">=20"},"peerDependencies":{"hono":">=4.0.0"},"repository":{"type":"git","url":"git+https://github.com/AWGMI/contact-guard.git"},"keywords":["contact-form","spam-protection","honeypot","rate-limiting","openrouter","resend"],"_id":"@awgmi/contact-guard@1.0.2","bugs":{"url":"https://github.com/AWGMI/contact-guard/issues"},"homepage":"https://github.com/AWGMI/contact-guard#readme","_integrity":"sha512-7tpzY6dfeeVPccwYCu6tClRAkaioaJ4wsn6ljCGXovWwowQiUWLlEnQsV4OneZE5GUykK+i+ju43dtU9sHOxEQ==","_resolved":"/tmp/723bae61ed0ac8d32dac37bf1b75a02a/awgmi-contact-guard-1.0.2.tgz","_from":"file:awgmi-contact-guard-1.0.2.tgz","_nodeVersion":"24.13.1","_npmVersion":"11.8.0","dist":{"integrity":"sha512-7tpzY6dfeeVPccwYCu6tClRAkaioaJ4wsn6ljCGXovWwowQiUWLlEnQsV4OneZE5GUykK+i+ju43dtU9sHOxEQ==","shasum":"a3ac15747ee61a6f35efc204f6afe8c922e13e2b","tarball":"https://registry.npmjs.org/@awgmi/contact-guard/-/contact-guard-1.0.2.tgz","fileCount":13,"unpackedSize":16570,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@awgmi%2fcontact-guard@1.0.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD1RxrhxmQwJ8C7n9OfNCTyPgn3CepvyEzm0fsb8UDCvwIgBJ/0brNZHWOnf5JX+guJOUwEzn7/x62vLuojwtcA2Yo="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:75a375a1-4aa4-4d09-9447-6f516152beb2"}},"directories":{},"maintainers":[{"name":"supakaity","email":"kaity@atikayda.au"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/contact-guard_1.0.2_1772456393364_0.5112018590767453"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-02T11:50:18.234Z","modified":"2026-03-02T12:59:54.108Z","1.0.0":"2026-03-02T11:50:18.471Z","1.0.1":"2026-03-02T12:24:03.649Z","1.0.2":"2026-03-02T12:59:53.518Z"},"bugs":{"url":"https://github.com/AWGMI/contact-guard/issues"},"license":"MIT","homepage":"https://github.com/AWGMI/contact-guard#readme","keywords":["contact-form","spam-protection","honeypot","rate-limiting","openrouter","resend"],"repository":{"type":"git","url":"git+https://github.com/AWGMI/contact-guard.git"},"description":"Zero-dependency spam classification for contact forms. OpenRouter + honeypot defences with fail-open design.","maintainers":[{"name":"supakaity","email":"kaity@atikayda.au"}],"readme":"# @awgmi/contact-guard\n\nA lightweight, zero-dependency spam classification library for contact forms. Uses OpenRouter's Gemini Flash Lite for LLM-based classification, with built-in honeypot middleware and prompt injection protection. Designed for fail-open reliability — if the classifier fails, legitimate messages get through. Includes Hono middleware for rate limiting and request validation, JSONL logging for submissions, and Resend integration for email delivery.\n\n## Usage\n\n```js\nimport { Hono } from 'hono'\nimport {\n  buildSystemPrompt,\n  createModelResolver,\n  createClassifier,\n  createSubmissionLogger,\n  createMailer,\n  createRateLimiter,\n  honeypotMiddleware,\n  HONEYPOT_HTML,\n  escapeHtml,\n  sanitizeForEmail,\n} from '@awgmi/contact-guard'\n\nconst app = new Hono()\n\nconst systemPrompt = buildSystemPrompt({\n  projectName: 'Acme',\n  projectDescription: 'a project management tool',\n  spamCategories: ['SEO services', 'Web design agencies', 'Link building'],\n  genuineCategories: ['Feature questions', 'Pricing inquiries', 'Bug reports'],\n  thanksGenuineExample: \"Thanks for reaching out about Acme!\",\n  thanksSpamExample: \"Thanks for your interest in helping businesses grow!\",\n})\n\nconst resolveModel = createModelResolver()\nconst classify = createClassifier({\n  apiKey: process.env.OPENROUTER_API_KEY,\n  systemPrompt,\n  resolveModel,\n})\nconst logSubmission = createSubmissionLogger({ baseDir: import.meta.dirname })\nconst sendEmail = createMailer({ apiKey: process.env.RESEND_API_KEY })\nconst rateLimit = createRateLimiter()\n\napp.post('/contact',\n  rateLimit,\n  honeypotMiddleware(),\n  async (c) => {\n    const body = c.get('parsedBody')\n    const name = body.name?.trim() || ''\n    const email = body.email?.trim() || ''\n    const message = body.message?.trim() || ''\n\n    const classification = await classify({ name, email, message })\n    logSubmission({ name, email, message, classification })\n\n    if (classification.classification === 'spam') return c.json({ success: true })\n\n    await sendEmail({\n      from: process.env.MAIL_FROM,\n      to: process.env.MAIL_TO,\n      replyTo: sanitizeForEmail(email),\n      subject: `Contact: ${sanitizeForEmail(name)}`,\n      text: `Name: ${name}\\nEmail: ${email}\\n\\n${message}`,\n      html: `<p><strong>${escapeHtml(name)}</strong> (${escapeHtml(email)})</p><p>${escapeHtml(message)}</p>`,\n    })\n\n    return c.json({ success: true })\n  }\n)\n```\n\nAdd the honeypot field to your HTML form:\n\n```js\nimport { HONEYPOT_HTML } from '@awgmi/contact-guard'\n// Insert HONEYPOT_HTML inside your <form> element\n```\n\n## Environment Variables\n\n| Variable | Required | Description |\n|---|---|---|\n| `OPENROUTER_API_KEY` | For classification | OpenRouter API key |\n| `RESEND_API_KEY` | For email | Resend API key |\n| `MAIL_FROM` | For email | Sender address |\n| `MAIL_TO` | For email | Recipient address |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}