feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: add email-otp plugin - #6

Open
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin
Open

feat: add email-otp plugin#6
EzraBuild wants to merge 1 commit into
thecodearcher:masterfrom
EzraBuild:feat/email-otp-plugin

Conversation

@EzraBuild

Copy link
Copy Markdown

Adds a passwordless email-OTP plugin (plugins/email-otp) modeled on
better-auth's email-otp plugin.

Endpoints

  • POST /email-otp/send-otp — request OTP (type: sign-in |
    email-verification)
  • POST /email-otp/sign-in — sign in with OTP, auto-creates user
  • POST /email-otp/verify-email — mark existing user's email as
    verified

Programmatic API via emailotp.Use(auth).SendOTP / SignInWithOTP / VerifyOTP.

Defaults (match better-auth)

  • 6-digit numeric codes
  • 5-minute expiry
  • 3 allowed verification attempts
  • Rate-limited 3/min per endpoint
  • Optional WithHashStoredOTP for at-rest HMAC-SHA256 hashing
    (improvement over better-auth's plain default; opt-in for parity)

Security

  • Transactional attempt counting via core.WithTransaction — race-safe
    under concurrent verifies
  • Anti-enumeration: send-otp returns generic success for unknown
    emails / sign-up-disabled mode
  • Type-scoped identifiers (sign-in-otp-<email> vs
    email-verification-otp-<email>) so codes never cross flows — covered
    by tests

Tests

23 unit tests covering happy paths, expiry, wrong-code lockout, attempt
counting, single-use consumption, auto-signup, additional fields
propagation, type isolation, hashed storage, input validation, and
handler-level cookie/status assertions. go test -race -count=1 passes.
go vet and gofmt clean.

Tested end-to-end

Manually verified against a Neon Postgres instance: send-otp → sign-in
(auto-creates user + sets session cookie) → authenticated /api/profile
call. Example added at examples/email-otp/.

Deferred to follow-up PRs

  • Password reset via OTP
  • Change-email flow
  • sendVerificationOnSignUp hook
  • overrideDefaultEmailVerification
  • OTP "reuse" resend strategy

Notes for the reviewer

  1. plugins/email-otp/go.mod carries replace github.com/thecodearcher/limen => ../.. so it builds before the next
    core release. Remove and bump to limen v0.1.2 when tagging, matching
    the sweep in commit efdafac.
  2. The example also fixes a latent routing bug — auth.Handler() was
    being mounted at /api/auth/ without setting
    WithHTTPBasePath("/api/auth"), so requests 404'd. This PR fixes it for
    examples/email-otp/ only; the other examples likely have the same bug
    and could use a separate cleanup PR.
  3. CI lint runs golangci-lint run ./... from root, which won't recurse
    into the plugin module. If you want the new plugin linted in CI, the
    workflow's lint step needs the same per-module loop the test step has —
    happy to follow up.

@thecodearcher

Copy link
Copy Markdown
Owner

Thanks for the contribution.

master has moved on a fair bit since this was opened, so this will need a rebase/update against the current tree before we can look at it properly. If the conflicts end up being more trouble than it’s worth, feel free to close and we can revisit later.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@EzraBuild@thecodearcher