oauth-callback 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,16 @@
1
- # OAuth Callback Server
1
+ # OAuth Callback
2
+
3
+ [![npm version](https://badge.fury.io/js/oauth-callback.svg)](https://badge.fury.io/js/oauth-callback)
4
+ [![npm downloads](https://img.shields.io/npm/dm/oauth-callback.svg)](https://npmjs.com/package/oauth-callback)
5
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kriasoft/oauth-callback/blob/main/LICENSE)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
2
7
 
3
8
  A lightweight local HTTP server for handling OAuth 2.0 authorization callbacks in Node.js, Deno, and Bun applications. Perfect for CLI tools, desktop applications, and development environments that need to capture OAuth authorization codes.
4
9
 
5
10
  ## Features
6
11
 
7
12
  - 🚀 **Multi-runtime support** - Works with Node.js 18+, Deno, and Bun
8
- - 🔒 **Secure localhost-only server** for OAuth callbacks
13
+ - 🔒 **Secure localhost-only server** for OAuth callbacks
9
14
  - ⚡ **Minimal dependencies** - Only requires `open` package
10
15
  - 🎯 **TypeScript support** out of the box
11
16
  - 🛡️ **Comprehensive OAuth error handling** with detailed error classes
@@ -212,6 +217,48 @@ class OAuthError extends Error {
212
217
  - No data is stored persistently
213
218
  - State parameter validation should be implemented by the application
214
219
 
220
+ ## Running the Examples
221
+
222
+ ### Interactive Demo (No Setup Required)
223
+
224
+ Try the library instantly with the built-in demo that includes a mock OAuth server:
225
+
226
+ ```bash
227
+ # Run the demo - no credentials needed!
228
+ bun run example:demo
229
+
230
+ # Run without opening browser (for CI/testing)
231
+ bun run examples/demo.ts --no-browser
232
+ ```
233
+
234
+ The demo showcases:
235
+
236
+ - Dynamic client registration (simplified OAuth 2.0 DCR)
237
+ - Complete authorization flow with mock provider
238
+ - Multiple scenarios (success, access denied, invalid scope)
239
+ - Custom HTML templates for success/error pages
240
+ - Token exchange and API usage simulation
241
+
242
+ ### Real OAuth Example (GitHub)
243
+
244
+ For testing with a real OAuth provider:
245
+
246
+ ```bash
247
+ # Set up GitHub OAuth App credentials
248
+ export GITHUB_CLIENT_ID="your_client_id"
249
+ export GITHUB_CLIENT_SECRET="your_client_secret"
250
+
251
+ # Run the GitHub example
252
+ bun run example:github
253
+ ```
254
+
255
+ This example demonstrates:
256
+
257
+ - Setting up OAuth with GitHub
258
+ - Handling the authorization callback
259
+ - Exchanging the code for an access token
260
+ - Using the token to fetch user information
261
+
215
262
  ## Development
216
263
 
217
264
  ```bash
@@ -222,10 +269,11 @@ bun install
222
269
  bun test
223
270
 
224
271
  # Build
225
- bun build ./src/index.ts --outdir ./dist
272
+ bun run build
226
273
 
227
- # Run example
228
- bun run example.ts
274
+ # Run examples
275
+ bun run example:demo # Interactive demo
276
+ bun run example:github # GitHub OAuth example
229
277
  ```
230
278
 
231
279
  ## Requirements
@@ -268,4 +316,4 @@ This project is released under the MIT License. Feel free to use it in your proj
268
316
 
269
317
  ## Support
270
318
 
271
- Found a bug or have a question? Please open an issue on the [GitHub issue tracker](https://github.com/koistya/oauth-callback/issues) and we'll be happy to help. If this project saves you time and you'd like to support its continued development, consider [becoming a sponsor](https://github.com/sponsors/koistya). Every bit of support helps maintain and improve this tool for the community. Thank you!
319
+ Found a bug or have a question? Please open an issue on the [GitHub issue tracker](https://github.com/kriasoft/oauth-callback/issues) and we'll be happy to help. If this project saves you time and you'd like to support its continued development, consider [becoming a sponsor](https://github.com/sponsors/koistya). Every bit of support helps maintain and improve this tool for the community. Thank you!
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGlD,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,kBAAkB,GAAG,MAAM,GACjC,OAAO,CAAC,cAAc,CAAC,CAsEzB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGlD,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,kBAAkB,GAAG,MAAM,GACjC,OAAO,CAAC,cAAc,CAAC,CAqEzB"}
package/dist/index.js CHANGED
@@ -513,22 +513,65 @@ class OAuthError extends Error {
513
513
  }
514
514
 
515
515
  // src/templates.ts
516
- var successTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>OAuth - Authorization Successful</title><link rel="stylesheet" href="./styles.css" /><script> // Auto-close window after 3 seconds let countdown = 3; function updateCountdown() { const element = document.getElementById("countdown"); if (element) { if (countdown > 0) { element.textContent = countdown + " second" + (countdown !== 1 ? "s" : ""); countdown--; setTimeout(updateCountdown, 1000); } else { element.innerHTML = '<span class="spinner"></span>Closing window...'; setTimeout(() => window.close(), 500); } } } window.addEventListener("DOMContentLoaded", updateCountdown); </script></head><body><div class="container"><div class="icon success">✓</div><h1>Authorization Successful!</h1><p>Your application has been successfully authorized.</p><p>You can now return to your application to continue.</p><div class="auto-close"><p class="text-muted text-small"> This window will close automatically in </p><div class="countdown" id="countdown">3 seconds</div></div></div></body></html>`;
517
- var errorTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>OAuth - {{ERROR_TITLE}}</title><link rel="stylesheet" href="./styles.css" /></head><body><div class="container"><div class="icon error">{{ERROR_ICON}}</div><h1>{{ERROR_TITLE}}</h1><p>The authorization process could not be completed.</p> {{ERROR_DETAILS}} <p class="text-muted mt-24">You can close this window and try again.</p></div></body></html>`;
516
+ var successTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Authorization Successful</title><style>:root{--system-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;--mono-font:ui-monospace,SFMono-Regular,"SF Mono",Consolas,"Liberation Mono",Menlo,monospace}@media (prefers-color-scheme:dark){:root{color-scheme:dark}}.minimal-body{margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:var(--system-font);background:linear-gradient(180deg,#fafafa 0%,#f0f0f0 100%);color:#1a1a1a;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dark .minimal-body{background:linear-gradient(180deg,#0a0a0a 0%,#1a1a1a 100%);color:#fafafa}.minimal-card{background:white;border-radius:16px;padding:48px 40px 40px;max-width:420px;width:90%;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.04),0 6px 16px rgba(0,0,0,0.08);animation:fadeUp 0.4s ease-out}.dark .minimal-card{background:#1f1f1f;box-shadow:0 1px 3px rgba(0,0,0,0.2),0 8px 24px rgba(0,0,0,0.4)}.minimal-title{font-size:24px;font-weight:600;margin:0 0 8px;letter-spacing:-0.02em;line-height:1.2}.minimal-subtitle{font-size:15px;color:#666;margin:0 0 32px;line-height:1.5}.dark .minimal-subtitle{color:#999}.success-icon-container{width:56px;height:56px;margin:0 auto 24px}.checkmark{width:56px;height:56px}.checkmark-circle{stroke:#10b981;stroke-width:2;stroke-miterlimit:10;fill:none}.checkmark-check{stroke:#10b981;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:48;stroke-dashoffset:48;fill:none}.checkmark.animate .checkmark-circle{animation:stroke 0.6s cubic-bezier(0.65,0,0.45,1) forwards}.checkmark.animate .checkmark-check{animation:stroke 0.3s cubic-bezier(0.65,0,0.45,1) 0.4s forwards}@keyframes stroke{to{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:shake 0.4s ease-out}@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}.countdown-container{margin-top:32px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .countdown-container{border-top-color:#333}.countdown-label{font-size:13px;color:#666;margin-bottom:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.05em}.dark .countdown-label{color:#999}#countdown-text{font-family:var(--mono-font);font-weight:600;color:#1a1a1a}.dark #countdown-text{color:#fafafa}.progress-track{width:100%;height:3px;background:#e5e5e5;border-radius:3px;overflow:hidden}.dark .progress-track{background:#333}.progress-bar{height:100%;background:linear-gradient(90deg,#10b981 0%,#059669 100%);border-radius:3px;transition:width 100ms linear;width:0}.minimal-actions{display:flex;gap:12px;margin-top:24px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .minimal-actions{border-top-color:#333}.minimal-button{flex:1;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer;transition:all 0.2s ease;font-family:var(--system-font);letter-spacing:-0.01em}.minimal-button:active{transform:scale(0.98)}.minimal-button.primary{background:#1a1a1a;color:white}.minimal-button.primary:hover{background:#000}.dark .minimal-button.primary{background:#fafafa;color:#1a1a1a}.dark .minimal-button.primary:hover{background:#fff}.minimal-button.secondary{background:#f5f5f5;color:#666}.minimal-button.secondary:hover{background:#e8e8e8}.dark .minimal-button.secondary{background:#2a2a2a;color:#999}.dark .minimal-button.secondary:hover{background:#333}.minimal-help{font-size:12px;color:#999;margin-top:16px;line-height:1.5}.dark .minimal-help{color:#666}.minimal-error-details{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;margin:20px 0;text-align:left;font-size:13px;line-height:1.6}.dark .minimal-error-details{background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.3)}.minimal-error-details strong{color:#dc2626;display:block;margin-bottom:4px;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:0.05em}.dark .minimal-error-details strong{color:#ef4444}.minimal-error-details code{font-family:var(--mono-font);background:rgba(0,0,0,0.05);padding:2px 6px;border-radius:4px;font-size:12px}.dark .minimal-error-details code{background:rgba(255,255,255,0.1)}@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important}}.minimal-button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (prefers-contrast:high){.minimal-card{border:2px solid currentColor}.minimal-button{border:1px solid currentColor}}</style><script> // Check for dark mode preference if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } // Auto-close window after 5 seconds const CLOSE_DELAY = 5000; let startTime = Date.now(); function updateCountdown() { const elapsed = Date.now() - startTime; const remaining = Math.max(0, CLOSE_DELAY - elapsed); const seconds = Math.ceil(remaining / 1000); const progress = Math.min(elapsed / CLOSE_DELAY, 1); const countdownEl = document.getElementById("countdown-text"); const progressEl = document.getElementById("progress-bar"); if (countdownEl && progressEl) { if (remaining > 0) { countdownEl.textContent = seconds + "s"; progressEl.style.width = progress * 100 + "%"; requestAnimationFrame(updateCountdown); } else { countdownEl.textContent = "Closing..."; setTimeout(() => window.close(), 100); } } } // Start when DOM is loaded window.addEventListener("DOMContentLoaded", () => { // Trigger check animation const checkmark = document.getElementById("checkmark"); if (checkmark) { setTimeout(() => checkmark.classList.add("animate"), 100); } // Start countdown requestAnimationFrame(updateCountdown); }); // Handle keyboard shortcuts document.addEventListener("keydown", (e) => { if (e.key === "Escape" || e.key === "Enter") { window.close(); } }); </script></head><body class="minimal-body"><div class="minimal-card"><div class="success-icon-container"><svg id="checkmark" class="checkmark" viewBox="0 0 52 52"><circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none" /><path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" /></svg></div><h1 class="minimal-title">Authorization Successful</h1><p class="minimal-subtitle">You can now return to your application</p><div class="countdown-container"><div class="countdown-label"> Closing in <span id="countdown-text">5s</span></div><div class="progress-track"><div id="progress-bar" class="progress-bar"></div></div></div></div></body></html>`;
517
+ var errorTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>{{ERROR_TITLE}}</title><style>:root{--system-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;--mono-font:ui-monospace,SFMono-Regular,"SF Mono",Consolas,"Liberation Mono",Menlo,monospace}@media (prefers-color-scheme:dark){:root{color-scheme:dark}}.minimal-body{margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:var(--system-font);background:linear-gradient(180deg,#fafafa 0%,#f0f0f0 100%);color:#1a1a1a;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dark .minimal-body{background:linear-gradient(180deg,#0a0a0a 0%,#1a1a1a 100%);color:#fafafa}.minimal-card{background:white;border-radius:16px;padding:48px 40px 40px;max-width:420px;width:90%;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.04),0 6px 16px rgba(0,0,0,0.08);animation:fadeUp 0.4s ease-out}.dark .minimal-card{background:#1f1f1f;box-shadow:0 1px 3px rgba(0,0,0,0.2),0 8px 24px rgba(0,0,0,0.4)}.minimal-title{font-size:24px;font-weight:600;margin:0 0 8px;letter-spacing:-0.02em;line-height:1.2}.minimal-subtitle{font-size:15px;color:#666;margin:0 0 32px;line-height:1.5}.dark .minimal-subtitle{color:#999}.success-icon-container{width:56px;height:56px;margin:0 auto 24px}.checkmark{width:56px;height:56px}.checkmark-circle{stroke:#10b981;stroke-width:2;stroke-miterlimit:10;fill:none}.checkmark-check{stroke:#10b981;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:48;stroke-dashoffset:48;fill:none}.checkmark.animate .checkmark-circle{animation:stroke 0.6s cubic-bezier(0.65,0,0.45,1) forwards}.checkmark.animate .checkmark-check{animation:stroke 0.3s cubic-bezier(0.65,0,0.45,1) 0.4s forwards}@keyframes stroke{to{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:shake 0.4s ease-out}@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}.countdown-container{margin-top:32px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .countdown-container{border-top-color:#333}.countdown-label{font-size:13px;color:#666;margin-bottom:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.05em}.dark .countdown-label{color:#999}#countdown-text{font-family:var(--mono-font);font-weight:600;color:#1a1a1a}.dark #countdown-text{color:#fafafa}.progress-track{width:100%;height:3px;background:#e5e5e5;border-radius:3px;overflow:hidden}.dark .progress-track{background:#333}.progress-bar{height:100%;background:linear-gradient(90deg,#10b981 0%,#059669 100%);border-radius:3px;transition:width 100ms linear;width:0}.minimal-actions{display:flex;gap:12px;margin-top:24px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .minimal-actions{border-top-color:#333}.minimal-button{flex:1;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer;transition:all 0.2s ease;font-family:var(--system-font);letter-spacing:-0.01em}.minimal-button:active{transform:scale(0.98)}.minimal-button.primary{background:#1a1a1a;color:white}.minimal-button.primary:hover{background:#000}.dark .minimal-button.primary{background:#fafafa;color:#1a1a1a}.dark .minimal-button.primary:hover{background:#fff}.minimal-button.secondary{background:#f5f5f5;color:#666}.minimal-button.secondary:hover{background:#e8e8e8}.dark .minimal-button.secondary{background:#2a2a2a;color:#999}.dark .minimal-button.secondary:hover{background:#333}.minimal-help{font-size:12px;color:#999;margin-top:16px;line-height:1.5}.dark .minimal-help{color:#666}.minimal-error-details{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;margin:20px 0;text-align:left;font-size:13px;line-height:1.6}.dark .minimal-error-details{background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.3)}.minimal-error-details strong{color:#dc2626;display:block;margin-bottom:4px;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:0.05em}.dark .minimal-error-details strong{color:#ef4444}.minimal-error-details code{font-family:var(--mono-font);background:rgba(0,0,0,0.05);padding:2px 6px;border-radius:4px;font-size:12px}.dark .minimal-error-details code{background:rgba(255,255,255,0.1)}@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important}}.minimal-button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (prefers-contrast:high){.minimal-card{border:2px solid currentColor}.minimal-button{border:1px solid currentColor}}</style><script> // Check for dark mode preference if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } // Start animations when DOM is loaded window.addEventListener("DOMContentLoaded", () => { // Animate error icon const errorIcon = document.getElementById("error-icon"); if (errorIcon) { setTimeout(() => errorIcon.classList.add("animate"), 100); } }); // Handle keyboard shortcuts document.addEventListener("keydown", (e) => { if (e.key === "Escape") { window.close(); } else if (e.key === "Enter") { window.location.reload(); } }); </script></head><body class="minimal-body"><div class="minimal-card"><div class="error-icon-container" id="error-icon">{{ERROR_SVG_ICON}}</div><h1 class="minimal-title">{{ERROR_TITLE}}</h1><p class="minimal-subtitle">{{ERROR_MESSAGE}}</p> {{ERROR_DETAILS}} <div class="minimal-actions"><button onclick="window.location.reload()" class="minimal-button primary" > Try Again </button><button onclick="window.close()" class="minimal-button secondary"> Close </button></div><p class="minimal-help">{{HELP_TEXT}}</p></div></body></html>`;
518
518
  function renderError(params) {
519
- const errorTitle = params.error === "access_denied" ? "Access Denied" : "Authorization Failed";
520
- const errorIcon = params.error === "access_denied" ? "\uD83D\uDEAB" : "⚠️";
519
+ let errorTitle = "Authorization Failed";
520
+ let errorMessage = "The authorization process could not be completed.";
521
+ let helpText = "If the problem persists, please contact support.";
522
+ let errorSvgIcon = "";
523
+ switch (params.error) {
524
+ case "access_denied":
525
+ errorTitle = "Access Denied";
526
+ errorMessage = "You have denied access to the application.";
527
+ helpText = 'Click "Try Again" to restart the authorization process.';
528
+ errorSvgIcon = `
529
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Access denied icon">
530
+ <path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
531
+ </svg>`;
532
+ break;
533
+ case "invalid_request":
534
+ errorTitle = "Invalid Request";
535
+ errorMessage = "The authorization request was malformed or missing required parameters.";
536
+ errorSvgIcon = `
537
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Invalid request icon">
538
+ <path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
539
+ </svg>`;
540
+ break;
541
+ case "unauthorized_client":
542
+ errorTitle = "Unauthorized Client";
543
+ errorMessage = "This application is not authorized to use this authentication method.";
544
+ errorSvgIcon = `
545
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Unauthorized icon">
546
+ <path d="M12 11V7a3 3 0 00-6 0v4m-1 0h8a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
547
+ </svg>`;
548
+ break;
549
+ case "server_error":
550
+ errorTitle = "Server Error";
551
+ errorMessage = "The authorization server encountered an unexpected error.";
552
+ helpText = "Please wait a moment and try again.";
553
+ errorSvgIcon = `
554
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Server error icon">
555
+ <path d="M12 9v6m0 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
556
+ </svg>`;
557
+ break;
558
+ default:
559
+ errorSvgIcon = `
560
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Warning icon">
561
+ <path d="M12 9v4m0 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
562
+ </svg>`;
563
+ }
521
564
  let errorDetails = "";
522
565
  if (params.error_description || params.error) {
523
566
  errorDetails = `
524
- <div class="error-details">
525
- <strong>Error Details:</strong>
567
+ <div class="minimal-error-details">
568
+ <strong>Error Code</strong>
526
569
  <code>${params.error}</code>
527
- ${params.error_description ? `<p class="mt-12">${params.error_description}</p>` : ""}
528
- ${params.error_uri ? `<p class="mt-8"><a href="${params.error_uri}" target="_blank">More information →</a></p>` : ""}
570
+ ${params.error_description ? `<p>${params.error_description}</p>` : ""}
571
+ ${params.error_uri ? `<p><a href="${params.error_uri}" target="_blank">More information →</a></p>` : ""}
529
572
  </div>`;
530
573
  }
531
- return errorTemplate.replace(/{{ERROR_TITLE}}/g, errorTitle).replace("{{ERROR_ICON}}", errorIcon).replace("{{ERROR_DETAILS}}", errorDetails);
574
+ return errorTemplate.replace(/{{ERROR_TITLE}}/g, errorTitle).replace("{{ERROR_ICON}}", "⚠️").replace("{{ERROR_SVG_ICON}}", errorSvgIcon).replace("{{ERROR_MESSAGE}}", errorMessage).replace("{{ERROR_DETAILS}}", errorDetails).replace("{{HELP_TEXT}}", helpText);
532
575
  }
533
576
 
534
577
  // src/server.ts
@@ -555,7 +598,14 @@ class BunCallbackServer {
555
598
  onRequest;
556
599
  abortHandler;
557
600
  async start(options) {
558
- const { port, hostname = "localhost", successHtml, errorHtml, signal, onRequest } = options;
601
+ const {
602
+ port,
603
+ hostname = "localhost",
604
+ successHtml,
605
+ errorHtml,
606
+ signal,
607
+ onRequest
608
+ } = options;
559
609
  this.successHtml = successHtml;
560
610
  this.errorHtml = errorHtml;
561
611
  this.onRequest = onRequest;
@@ -656,7 +706,14 @@ class DenoCallbackServer {
656
706
  onRequest;
657
707
  abortHandler;
658
708
  async start(options) {
659
- const { port, hostname = "localhost", successHtml, errorHtml, signal, onRequest } = options;
709
+ const {
710
+ port,
711
+ hostname = "localhost",
712
+ successHtml,
713
+ errorHtml,
714
+ signal,
715
+ onRequest
716
+ } = options;
660
717
  this.successHtml = successHtml;
661
718
  this.errorHtml = errorHtml;
662
719
  this.onRequest = onRequest;
@@ -754,7 +811,14 @@ class NodeCallbackServer {
754
811
  onRequest;
755
812
  abortHandler;
756
813
  async start(options) {
757
- const { port, hostname = "localhost", successHtml, errorHtml, signal, onRequest } = options;
814
+ const {
815
+ port,
816
+ hostname = "localhost",
817
+ successHtml,
818
+ errorHtml,
819
+ signal,
820
+ onRequest
821
+ } = options;
758
822
  this.successHtml = successHtml;
759
823
  this.errorHtml = errorHtml;
760
824
  this.onRequest = onRequest;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mHAAmH;IACnH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,iEAAiE;IACjE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxE,4CAA4C;IAC5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAmeD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAarD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mHAAmH;IACnH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,iEAAiE;IACjE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxE,4CAA4C;IAC5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAuhBD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAarD"}
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Auto-generated OAuth callback HTML templates
3
- * Generated from assets/*.html and assets/*.css
3
+ * Generated from templates/*.html and templates/*.css
4
4
  *
5
- * To regenerate: bun run assets/build.ts
5
+ * To regenerate: bun run templates/build.ts
6
6
  */
7
- export declare const successTemplate = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /><title>OAuth - Authorization Successful</title><link rel=\"stylesheet\" href=\"./styles.css\" /><script> // Auto-close window after 3 seconds let countdown = 3; function updateCountdown() { const element = document.getElementById(\"countdown\"); if (element) { if (countdown > 0) { element.textContent = countdown + \" second\" + (countdown !== 1 ? \"s\" : \"\"); countdown--; setTimeout(updateCountdown, 1000); } else { element.innerHTML = '<span class=\"spinner\"></span>Closing window...'; setTimeout(() => window.close(), 500); } } } window.addEventListener(\"DOMContentLoaded\", updateCountdown); </script></head><body><div class=\"container\"><div class=\"icon success\">\u2713</div><h1>Authorization Successful!</h1><p>Your application has been successfully authorized.</p><p>You can now return to your application to continue.</p><div class=\"auto-close\"><p class=\"text-muted text-small\"> This window will close automatically in </p><div class=\"countdown\" id=\"countdown\">3 seconds</div></div></div></body></html>";
8
- export declare const errorTemplate = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /><title>OAuth - {{ERROR_TITLE}}</title><link rel=\"stylesheet\" href=\"./styles.css\" /></head><body><div class=\"container\"><div class=\"icon error\">{{ERROR_ICON}}</div><h1>{{ERROR_TITLE}}</h1><p>The authorization process could not be completed.</p> {{ERROR_DETAILS}} <p class=\"text-muted mt-24\">You can close this window and try again.</p></div></body></html>";
7
+ export declare const successTemplate = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /><title>Authorization Successful</title><style>:root{--system-font:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Oxygen,Ubuntu,Cantarell,\"Helvetica Neue\",sans-serif;--mono-font:ui-monospace,SFMono-Regular,\"SF Mono\",Consolas,\"Liberation Mono\",Menlo,monospace}@media (prefers-color-scheme:dark){:root{color-scheme:dark}}.minimal-body{margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:var(--system-font);background:linear-gradient(180deg,#fafafa 0%,#f0f0f0 100%);color:#1a1a1a;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dark .minimal-body{background:linear-gradient(180deg,#0a0a0a 0%,#1a1a1a 100%);color:#fafafa}.minimal-card{background:white;border-radius:16px;padding:48px 40px 40px;max-width:420px;width:90%;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.04),0 6px 16px rgba(0,0,0,0.08);animation:fadeUp 0.4s ease-out}.dark .minimal-card{background:#1f1f1f;box-shadow:0 1px 3px rgba(0,0,0,0.2),0 8px 24px rgba(0,0,0,0.4)}.minimal-title{font-size:24px;font-weight:600;margin:0 0 8px;letter-spacing:-0.02em;line-height:1.2}.minimal-subtitle{font-size:15px;color:#666;margin:0 0 32px;line-height:1.5}.dark .minimal-subtitle{color:#999}.success-icon-container{width:56px;height:56px;margin:0 auto 24px}.checkmark{width:56px;height:56px}.checkmark-circle{stroke:#10b981;stroke-width:2;stroke-miterlimit:10;fill:none}.checkmark-check{stroke:#10b981;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:48;stroke-dashoffset:48;fill:none}.checkmark.animate .checkmark-circle{animation:stroke 0.6s cubic-bezier(0.65,0,0.45,1) forwards}.checkmark.animate .checkmark-check{animation:stroke 0.3s cubic-bezier(0.65,0,0.45,1) 0.4s forwards}@keyframes stroke{to{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:shake 0.4s ease-out}@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}.countdown-container{margin-top:32px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .countdown-container{border-top-color:#333}.countdown-label{font-size:13px;color:#666;margin-bottom:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.05em}.dark .countdown-label{color:#999}#countdown-text{font-family:var(--mono-font);font-weight:600;color:#1a1a1a}.dark #countdown-text{color:#fafafa}.progress-track{width:100%;height:3px;background:#e5e5e5;border-radius:3px;overflow:hidden}.dark .progress-track{background:#333}.progress-bar{height:100%;background:linear-gradient(90deg,#10b981 0%,#059669 100%);border-radius:3px;transition:width 100ms linear;width:0}.minimal-actions{display:flex;gap:12px;margin-top:24px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .minimal-actions{border-top-color:#333}.minimal-button{flex:1;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer;transition:all 0.2s ease;font-family:var(--system-font);letter-spacing:-0.01em}.minimal-button:active{transform:scale(0.98)}.minimal-button.primary{background:#1a1a1a;color:white}.minimal-button.primary:hover{background:#000}.dark .minimal-button.primary{background:#fafafa;color:#1a1a1a}.dark .minimal-button.primary:hover{background:#fff}.minimal-button.secondary{background:#f5f5f5;color:#666}.minimal-button.secondary:hover{background:#e8e8e8}.dark .minimal-button.secondary{background:#2a2a2a;color:#999}.dark .minimal-button.secondary:hover{background:#333}.minimal-help{font-size:12px;color:#999;margin-top:16px;line-height:1.5}.dark .minimal-help{color:#666}.minimal-error-details{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;margin:20px 0;text-align:left;font-size:13px;line-height:1.6}.dark .minimal-error-details{background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.3)}.minimal-error-details strong{color:#dc2626;display:block;margin-bottom:4px;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:0.05em}.dark .minimal-error-details strong{color:#ef4444}.minimal-error-details code{font-family:var(--mono-font);background:rgba(0,0,0,0.05);padding:2px 6px;border-radius:4px;font-size:12px}.dark .minimal-error-details code{background:rgba(255,255,255,0.1)}@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important}}.minimal-button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (prefers-contrast:high){.minimal-card{border:2px solid currentColor}.minimal-button{border:1px solid currentColor}}</style><script> // Check for dark mode preference if (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) { document.documentElement.classList.add(\"dark\"); } // Auto-close window after 5 seconds const CLOSE_DELAY = 5000; let startTime = Date.now(); function updateCountdown() { const elapsed = Date.now() - startTime; const remaining = Math.max(0, CLOSE_DELAY - elapsed); const seconds = Math.ceil(remaining / 1000); const progress = Math.min(elapsed / CLOSE_DELAY, 1); const countdownEl = document.getElementById(\"countdown-text\"); const progressEl = document.getElementById(\"progress-bar\"); if (countdownEl && progressEl) { if (remaining > 0) { countdownEl.textContent = seconds + \"s\"; progressEl.style.width = progress * 100 + \"%\"; requestAnimationFrame(updateCountdown); } else { countdownEl.textContent = \"Closing...\"; setTimeout(() => window.close(), 100); } } } // Start when DOM is loaded window.addEventListener(\"DOMContentLoaded\", () => { // Trigger check animation const checkmark = document.getElementById(\"checkmark\"); if (checkmark) { setTimeout(() => checkmark.classList.add(\"animate\"), 100); } // Start countdown requestAnimationFrame(updateCountdown); }); // Handle keyboard shortcuts document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\" || e.key === \"Enter\") { window.close(); } }); </script></head><body class=\"minimal-body\"><div class=\"minimal-card\"><div class=\"success-icon-container\"><svg id=\"checkmark\" class=\"checkmark\" viewBox=\"0 0 52 52\"><circle class=\"checkmark-circle\" cx=\"26\" cy=\"26\" r=\"25\" fill=\"none\" /><path class=\"checkmark-check\" fill=\"none\" d=\"M14.1 27.2l7.1 7.2 16.7-16.8\" /></svg></div><h1 class=\"minimal-title\">Authorization Successful</h1><p class=\"minimal-subtitle\">You can now return to your application</p><div class=\"countdown-container\"><div class=\"countdown-label\"> Closing in <span id=\"countdown-text\">5s</span></div><div class=\"progress-track\"><div id=\"progress-bar\" class=\"progress-bar\"></div></div></div></div></body></html>";
8
+ export declare const errorTemplate = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /><title>{{ERROR_TITLE}}</title><style>:root{--system-font:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Oxygen,Ubuntu,Cantarell,\"Helvetica Neue\",sans-serif;--mono-font:ui-monospace,SFMono-Regular,\"SF Mono\",Consolas,\"Liberation Mono\",Menlo,monospace}@media (prefers-color-scheme:dark){:root{color-scheme:dark}}.minimal-body{margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:var(--system-font);background:linear-gradient(180deg,#fafafa 0%,#f0f0f0 100%);color:#1a1a1a;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dark .minimal-body{background:linear-gradient(180deg,#0a0a0a 0%,#1a1a1a 100%);color:#fafafa}.minimal-card{background:white;border-radius:16px;padding:48px 40px 40px;max-width:420px;width:90%;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.04),0 6px 16px rgba(0,0,0,0.08);animation:fadeUp 0.4s ease-out}.dark .minimal-card{background:#1f1f1f;box-shadow:0 1px 3px rgba(0,0,0,0.2),0 8px 24px rgba(0,0,0,0.4)}.minimal-title{font-size:24px;font-weight:600;margin:0 0 8px;letter-spacing:-0.02em;line-height:1.2}.minimal-subtitle{font-size:15px;color:#666;margin:0 0 32px;line-height:1.5}.dark .minimal-subtitle{color:#999}.success-icon-container{width:56px;height:56px;margin:0 auto 24px}.checkmark{width:56px;height:56px}.checkmark-circle{stroke:#10b981;stroke-width:2;stroke-miterlimit:10;fill:none}.checkmark-check{stroke:#10b981;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:48;stroke-dashoffset:48;fill:none}.checkmark.animate .checkmark-circle{animation:stroke 0.6s cubic-bezier(0.65,0,0.45,1) forwards}.checkmark.animate .checkmark-check{animation:stroke 0.3s cubic-bezier(0.65,0,0.45,1) 0.4s forwards}@keyframes stroke{to{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:shake 0.4s ease-out}@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}.countdown-container{margin-top:32px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .countdown-container{border-top-color:#333}.countdown-label{font-size:13px;color:#666;margin-bottom:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.05em}.dark .countdown-label{color:#999}#countdown-text{font-family:var(--mono-font);font-weight:600;color:#1a1a1a}.dark #countdown-text{color:#fafafa}.progress-track{width:100%;height:3px;background:#e5e5e5;border-radius:3px;overflow:hidden}.dark .progress-track{background:#333}.progress-bar{height:100%;background:linear-gradient(90deg,#10b981 0%,#059669 100%);border-radius:3px;transition:width 100ms linear;width:0}.minimal-actions{display:flex;gap:12px;margin-top:24px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .minimal-actions{border-top-color:#333}.minimal-button{flex:1;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer;transition:all 0.2s ease;font-family:var(--system-font);letter-spacing:-0.01em}.minimal-button:active{transform:scale(0.98)}.minimal-button.primary{background:#1a1a1a;color:white}.minimal-button.primary:hover{background:#000}.dark .minimal-button.primary{background:#fafafa;color:#1a1a1a}.dark .minimal-button.primary:hover{background:#fff}.minimal-button.secondary{background:#f5f5f5;color:#666}.minimal-button.secondary:hover{background:#e8e8e8}.dark .minimal-button.secondary{background:#2a2a2a;color:#999}.dark .minimal-button.secondary:hover{background:#333}.minimal-help{font-size:12px;color:#999;margin-top:16px;line-height:1.5}.dark .minimal-help{color:#666}.minimal-error-details{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;margin:20px 0;text-align:left;font-size:13px;line-height:1.6}.dark .minimal-error-details{background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.3)}.minimal-error-details strong{color:#dc2626;display:block;margin-bottom:4px;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:0.05em}.dark .minimal-error-details strong{color:#ef4444}.minimal-error-details code{font-family:var(--mono-font);background:rgba(0,0,0,0.05);padding:2px 6px;border-radius:4px;font-size:12px}.dark .minimal-error-details code{background:rgba(255,255,255,0.1)}@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important}}.minimal-button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (prefers-contrast:high){.minimal-card{border:2px solid currentColor}.minimal-button{border:1px solid currentColor}}</style><script> // Check for dark mode preference if (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) { document.documentElement.classList.add(\"dark\"); } // Start animations when DOM is loaded window.addEventListener(\"DOMContentLoaded\", () => { // Animate error icon const errorIcon = document.getElementById(\"error-icon\"); if (errorIcon) { setTimeout(() => errorIcon.classList.add(\"animate\"), 100); } }); // Handle keyboard shortcuts document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") { window.close(); } else if (e.key === \"Enter\") { window.location.reload(); } }); </script></head><body class=\"minimal-body\"><div class=\"minimal-card\"><div class=\"error-icon-container\" id=\"error-icon\">{{ERROR_SVG_ICON}}</div><h1 class=\"minimal-title\">{{ERROR_TITLE}}</h1><p class=\"minimal-subtitle\">{{ERROR_MESSAGE}}</p> {{ERROR_DETAILS}} <div class=\"minimal-actions\"><button onclick=\"window.location.reload()\" class=\"minimal-button primary\" > Try Again </button><button onclick=\"window.close()\" class=\"minimal-button secondary\"> Close </button></div><p class=\"minimal-help\">{{HELP_TEXT}}</p></div></body></html>";
9
9
  export declare function renderError(params: {
10
10
  error: string;
11
11
  error_description?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,eAAe,qpCAA4mC,CAAC;AAEzoC,eAAO,MAAM,aAAa,8fAA4e,CAAC;AAEvgB,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAmBT"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,eAAe,20NAAqvN,CAAC;AAElxN,eAAO,MAAM,aAAa,g8LAAw4L,CAAC;AAEn6L,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAmET"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth-callback",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Simple local OAuth callback server for capturing authorization codes in CLI tools and desktop apps",
5
5
  "keywords": [
6
6
  "oauth",
@@ -73,8 +73,8 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@types/bun": "latest",
76
- "typescript": "^5.9.2",
77
- "prettier": "^3.6.2"
76
+ "prettier": "^3.6.2",
77
+ "typescript": "^5.9.2"
78
78
  },
79
79
  "prettier": {
80
80
  "printWidth": 80,
@@ -87,6 +87,8 @@
87
87
  "build:templates": "bun run templates/build.ts",
88
88
  "build": "bun run build:templates && bun build ./src/index.ts --outdir=./dist --target=node && tsc --declaration --emitDeclarationOnly --outDir ./dist",
89
89
  "prepublishOnly": "bun run build",
90
- "test": "bun test"
90
+ "test": "bun test",
91
+ "example:demo": "bun run examples/demo.ts",
92
+ "example:github": "bun run examples/github.ts"
91
93
  }
92
94
  }
package/src/errors.ts CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  /**
5
5
  * Custom error class for OAuth-specific errors
6
- *
6
+ *
7
7
  * This error is thrown when the OAuth provider returns an error response
8
8
  * (e.g., user denies access, invalid client, etc.). It preserves the
9
9
  * original OAuth error details for proper error handling.
@@ -33,7 +33,7 @@ export class OAuthError extends Error {
33
33
 
34
34
  /**
35
35
  * Error thrown when OAuth callback times out
36
- *
36
+ *
37
37
  * This error indicates that no OAuth callback was received within the
38
38
  * specified timeout period. This could happen if the user doesn't complete
39
39
  * the authorization flow or if there are network connectivity issues.
@@ -47,4 +47,4 @@ export class TimeoutError extends Error {
47
47
  super(message);
48
48
  this.name = "TimeoutError";
49
49
  }
50
- }
50
+ }
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  /**
5
5
  * @fileoverview OAuth 2.0 authorization code flow handler for Node.js, Deno, and Bun
6
- *
6
+ *
7
7
  * This module provides a simple way to handle OAuth 2.0 authorization code flows
8
8
  * in command-line applications, desktop apps, and development environments.
9
9
  * It creates a temporary local HTTP server to capture the authorization callback.
@@ -21,7 +21,7 @@ export type { GetAuthCodeOptions } from "./types";
21
21
 
22
22
  /**
23
23
  * Get the OAuth authorization code from the authorization URL.
24
- *
24
+ *
25
25
  * Creates a temporary local HTTP server to handle the OAuth callback,
26
26
  * opens the authorization URL in the user's browser, and waits for the
27
27
  * OAuth provider to redirect back with an authorization code.
@@ -32,13 +32,13 @@ export type { GetAuthCodeOptions } from "./types";
32
32
  * and other parameters, or rejects with OAuthError if authorization fails
33
33
  * @throws {OAuthError} When OAuth provider returns an error (e.g., access_denied)
34
34
  * @throws {Error} For timeout, network errors, or other unexpected failures
35
- *
35
+ *
36
36
  * @example
37
37
  * ```typescript
38
38
  * // Simple usage with URL string
39
39
  * const result = await getAuthCode('https://oauth.example.com/authorize?...');
40
40
  * console.log('Code:', result.code);
41
- *
41
+ *
42
42
  * // Advanced usage with options
43
43
  * const result = await getAuthCode({
44
44
  * authorizationUrl: 'https://oauth.example.com/authorize?...',
@@ -51,10 +51,9 @@ export type { GetAuthCodeOptions } from "./types";
51
51
  export async function getAuthCode(
52
52
  input: GetAuthCodeOptions | string,
53
53
  ): Promise<CallbackResult> {
54
- const options: GetAuthCodeOptions = typeof input === "string"
55
- ? { authorizationUrl: input }
56
- : input;
57
-
54
+ const options: GetAuthCodeOptions =
55
+ typeof input === "string" ? { authorizationUrl: input } : input;
56
+
58
57
  const {
59
58
  authorizationUrl,
60
59
  port = 3000,
package/src/server.ts CHANGED
@@ -62,14 +62,18 @@ export interface CallbackServer {
62
62
  * @param errorHtml - Custom error HTML template with placeholder support
63
63
  * @returns Rendered HTML content
64
64
  */
65
- function generateCallbackHTML(params: CallbackResult, successHtml?: string, errorHtml?: string): string {
65
+ function generateCallbackHTML(
66
+ params: CallbackResult,
67
+ successHtml?: string,
68
+ errorHtml?: string,
69
+ ): string {
66
70
  if (params.error) {
67
71
  // Use custom error HTML if provided
68
72
  if (errorHtml) {
69
73
  return errorHtml
70
- .replace(/{{error}}/g, params.error || '')
71
- .replace(/{{error_description}}/g, params.error_description || '')
72
- .replace(/{{error_uri}}/g, params.error_uri || '');
74
+ .replace(/{{error}}/g, params.error || "")
75
+ .replace(/{{error_description}}/g, params.error_description || "")
76
+ .replace(/{{error_uri}}/g, params.error_uri || "");
73
77
  }
74
78
  return renderError({
75
79
  error: params.error,
@@ -97,12 +101,19 @@ class BunCallbackServer implements CallbackServer {
97
101
  private abortHandler?: () => void;
98
102
 
99
103
  async start(options: ServerOptions): Promise<void> {
100
- const { port, hostname = "localhost", successHtml, errorHtml, signal, onRequest } = options;
101
-
104
+ const {
105
+ port,
106
+ hostname = "localhost",
107
+ successHtml,
108
+ errorHtml,
109
+ signal,
110
+ onRequest,
111
+ } = options;
112
+
102
113
  this.successHtml = successHtml;
103
114
  this.errorHtml = errorHtml;
104
115
  this.onRequest = onRequest;
105
-
116
+
106
117
  // Handle abort signal
107
118
  if (signal) {
108
119
  if (signal.aborted) {
@@ -116,7 +127,7 @@ class BunCallbackServer implements CallbackServer {
116
127
  };
117
128
  signal.addEventListener("abort", this.abortHandler);
118
129
  }
119
-
130
+
120
131
  // @ts-ignore - Bun global not available in TypeScript definitions
121
132
  this.server = Bun.serve({
122
133
  port,
@@ -130,7 +141,7 @@ class BunCallbackServer implements CallbackServer {
130
141
  if (this.onRequest) {
131
142
  this.onRequest(request);
132
143
  }
133
-
144
+
134
145
  const url = new URL(request.url);
135
146
 
136
147
  if (url.pathname === this.callbackPath) {
@@ -147,10 +158,13 @@ class BunCallbackServer implements CallbackServer {
147
158
  }
148
159
 
149
160
  // Return success or error HTML page
150
- return new Response(generateCallbackHTML(params, this.successHtml, this.errorHtml), {
151
- status: 200,
152
- headers: { "Content-Type": "text/html" },
153
- });
161
+ return new Response(
162
+ generateCallbackHTML(params, this.successHtml, this.errorHtml),
163
+ {
164
+ status: 200,
165
+ headers: { "Content-Type": "text/html" },
166
+ },
167
+ );
154
168
  }
155
169
 
156
170
  return new Response("Not Found", { status: 404 });
@@ -164,12 +178,16 @@ class BunCallbackServer implements CallbackServer {
164
178
 
165
179
  return new Promise((resolve, reject) => {
166
180
  let isResolved = false;
167
-
181
+
168
182
  const timer = setTimeout(() => {
169
183
  if (!isResolved) {
170
184
  isResolved = true;
171
185
  this.callbackPromise = undefined;
172
- reject(new Error(`OAuth callback timeout after ${timeout}ms waiting for ${path}`));
186
+ reject(
187
+ new Error(
188
+ `OAuth callback timeout after ${timeout}ms waiting for ${path}`,
189
+ ),
190
+ );
173
191
  }
174
192
  }, timeout);
175
193
 
@@ -205,7 +223,9 @@ class BunCallbackServer implements CallbackServer {
205
223
  this.abortHandler = undefined;
206
224
  }
207
225
  if (this.callbackPromise) {
208
- this.callbackPromise.reject(new Error("Server stopped before callback received"));
226
+ this.callbackPromise.reject(
227
+ new Error("Server stopped before callback received"),
228
+ );
209
229
  this.callbackPromise = undefined;
210
230
  }
211
231
  if (this.server) {
@@ -232,14 +252,21 @@ class DenoCallbackServer implements CallbackServer {
232
252
  private abortHandler?: () => void;
233
253
 
234
254
  async start(options: ServerOptions): Promise<void> {
235
- const { port, hostname = "localhost", successHtml, errorHtml, signal, onRequest } = options;
236
-
255
+ const {
256
+ port,
257
+ hostname = "localhost",
258
+ successHtml,
259
+ errorHtml,
260
+ signal,
261
+ onRequest,
262
+ } = options;
263
+
237
264
  this.successHtml = successHtml;
238
265
  this.errorHtml = errorHtml;
239
266
  this.onRequest = onRequest;
240
-
267
+
241
268
  this.abortController = new AbortController();
242
-
269
+
243
270
  // Handle user's abort signal
244
271
  if (signal) {
245
272
  if (signal.aborted) {
@@ -266,7 +293,7 @@ class DenoCallbackServer implements CallbackServer {
266
293
  if (this.onRequest) {
267
294
  this.onRequest(request);
268
295
  }
269
-
296
+
270
297
  const url = new URL(request.url);
271
298
 
272
299
  if (url.pathname === this.callbackPath) {
@@ -283,10 +310,13 @@ class DenoCallbackServer implements CallbackServer {
283
310
  }
284
311
 
285
312
  // Return success or error HTML page
286
- return new Response(generateCallbackHTML(params, this.successHtml, this.errorHtml), {
287
- status: 200,
288
- headers: { "Content-Type": "text/html" },
289
- });
313
+ return new Response(
314
+ generateCallbackHTML(params, this.successHtml, this.errorHtml),
315
+ {
316
+ status: 200,
317
+ headers: { "Content-Type": "text/html" },
318
+ },
319
+ );
290
320
  }
291
321
 
292
322
  return new Response("Not Found", { status: 404 });
@@ -300,12 +330,16 @@ class DenoCallbackServer implements CallbackServer {
300
330
 
301
331
  return new Promise((resolve, reject) => {
302
332
  let isResolved = false;
303
-
333
+
304
334
  const timer = setTimeout(() => {
305
335
  if (!isResolved) {
306
336
  isResolved = true;
307
337
  this.callbackPromise = undefined;
308
- reject(new Error(`OAuth callback timeout after ${timeout}ms waiting for ${path}`));
338
+ reject(
339
+ new Error(
340
+ `OAuth callback timeout after ${timeout}ms waiting for ${path}`,
341
+ ),
342
+ );
309
343
  }
310
344
  }, timeout);
311
345
 
@@ -341,7 +375,9 @@ class DenoCallbackServer implements CallbackServer {
341
375
  this.abortHandler = undefined;
342
376
  }
343
377
  if (this.callbackPromise) {
344
- this.callbackPromise.reject(new Error("Server stopped before callback received"));
378
+ this.callbackPromise.reject(
379
+ new Error("Server stopped before callback received"),
380
+ );
345
381
  this.callbackPromise = undefined;
346
382
  }
347
383
  if (this.abortController) {
@@ -369,12 +405,19 @@ class NodeCallbackServer implements CallbackServer {
369
405
  private abortHandler?: () => void;
370
406
 
371
407
  async start(options: ServerOptions): Promise<void> {
372
- const { port, hostname = "localhost", successHtml, errorHtml, signal, onRequest } = options;
373
-
408
+ const {
409
+ port,
410
+ hostname = "localhost",
411
+ successHtml,
412
+ errorHtml,
413
+ signal,
414
+ onRequest,
415
+ } = options;
416
+
374
417
  this.successHtml = successHtml;
375
418
  this.errorHtml = errorHtml;
376
419
  this.onRequest = onRequest;
377
-
420
+
378
421
  // Handle abort signal
379
422
  if (signal) {
380
423
  if (signal.aborted) {
@@ -388,7 +431,7 @@ class NodeCallbackServer implements CallbackServer {
388
431
  };
389
432
  signal.addEventListener("abort", this.abortHandler);
390
433
  }
391
-
434
+
392
435
  const { createServer } = await import("node:http");
393
436
 
394
437
  return new Promise((resolve, reject) => {
@@ -449,7 +492,7 @@ class NodeCallbackServer implements CallbackServer {
449
492
  if (this.onRequest) {
450
493
  this.onRequest(request);
451
494
  }
452
-
495
+
453
496
  const url = new URL(request.url);
454
497
 
455
498
  if (url.pathname === this.callbackPath) {
@@ -466,10 +509,13 @@ class NodeCallbackServer implements CallbackServer {
466
509
  }
467
510
 
468
511
  // Return success or error HTML page
469
- return new Response(generateCallbackHTML(params, this.successHtml, this.errorHtml), {
470
- status: 200,
471
- headers: { "Content-Type": "text/html" },
472
- });
512
+ return new Response(
513
+ generateCallbackHTML(params, this.successHtml, this.errorHtml),
514
+ {
515
+ status: 200,
516
+ headers: { "Content-Type": "text/html" },
517
+ },
518
+ );
473
519
  }
474
520
 
475
521
  return new Response("Not Found", { status: 404 });
@@ -483,12 +529,16 @@ class NodeCallbackServer implements CallbackServer {
483
529
 
484
530
  return new Promise((resolve, reject) => {
485
531
  let isResolved = false;
486
-
532
+
487
533
  const timer = setTimeout(() => {
488
534
  if (!isResolved) {
489
535
  isResolved = true;
490
536
  this.callbackPromise = undefined;
491
- reject(new Error(`OAuth callback timeout after ${timeout}ms waiting for ${path}`));
537
+ reject(
538
+ new Error(
539
+ `OAuth callback timeout after ${timeout}ms waiting for ${path}`,
540
+ ),
541
+ );
492
542
  }
493
543
  }, timeout);
494
544
 
@@ -524,7 +574,9 @@ class NodeCallbackServer implements CallbackServer {
524
574
  this.abortHandler = undefined;
525
575
  }
526
576
  if (this.callbackPromise) {
527
- this.callbackPromise.reject(new Error("Server stopped before callback received"));
577
+ this.callbackPromise.reject(
578
+ new Error("Server stopped before callback received"),
579
+ );
528
580
  this.callbackPromise = undefined;
529
581
  }
530
582
  if (this.server) {
package/src/templates.ts CHANGED
@@ -1,35 +1,83 @@
1
1
  /**
2
2
  * Auto-generated OAuth callback HTML templates
3
- * Generated from assets/*.html and assets/*.css
3
+ * Generated from templates/*.html and templates/*.css
4
4
  *
5
- * To regenerate: bun run assets/build.ts
5
+ * To regenerate: bun run templates/build.ts
6
6
  */
7
7
 
8
- export const successTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>OAuth - Authorization Successful</title><link rel="stylesheet" href="./styles.css" /><script> // Auto-close window after 3 seconds let countdown = 3; function updateCountdown() { const element = document.getElementById("countdown"); if (element) { if (countdown > 0) { element.textContent = countdown + " second" + (countdown !== 1 ? "s" : ""); countdown--; setTimeout(updateCountdown, 1000); } else { element.innerHTML = '<span class="spinner"></span>Closing window...'; setTimeout(() => window.close(), 500); } } } window.addEventListener("DOMContentLoaded", updateCountdown); </script></head><body><div class="container"><div class="icon success">✓</div><h1>Authorization Successful!</h1><p>Your application has been successfully authorized.</p><p>You can now return to your application to continue.</p><div class="auto-close"><p class="text-muted text-small"> This window will close automatically in </p><div class="countdown" id="countdown">3 seconds</div></div></div></body></html>`;
8
+ export const successTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Authorization Successful</title><style>:root{--system-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;--mono-font:ui-monospace,SFMono-Regular,"SF Mono",Consolas,"Liberation Mono",Menlo,monospace}@media (prefers-color-scheme:dark){:root{color-scheme:dark}}.minimal-body{margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:var(--system-font);background:linear-gradient(180deg,#fafafa 0%,#f0f0f0 100%);color:#1a1a1a;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dark .minimal-body{background:linear-gradient(180deg,#0a0a0a 0%,#1a1a1a 100%);color:#fafafa}.minimal-card{background:white;border-radius:16px;padding:48px 40px 40px;max-width:420px;width:90%;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.04),0 6px 16px rgba(0,0,0,0.08);animation:fadeUp 0.4s ease-out}.dark .minimal-card{background:#1f1f1f;box-shadow:0 1px 3px rgba(0,0,0,0.2),0 8px 24px rgba(0,0,0,0.4)}.minimal-title{font-size:24px;font-weight:600;margin:0 0 8px;letter-spacing:-0.02em;line-height:1.2}.minimal-subtitle{font-size:15px;color:#666;margin:0 0 32px;line-height:1.5}.dark .minimal-subtitle{color:#999}.success-icon-container{width:56px;height:56px;margin:0 auto 24px}.checkmark{width:56px;height:56px}.checkmark-circle{stroke:#10b981;stroke-width:2;stroke-miterlimit:10;fill:none}.checkmark-check{stroke:#10b981;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:48;stroke-dashoffset:48;fill:none}.checkmark.animate .checkmark-circle{animation:stroke 0.6s cubic-bezier(0.65,0,0.45,1) forwards}.checkmark.animate .checkmark-check{animation:stroke 0.3s cubic-bezier(0.65,0,0.45,1) 0.4s forwards}@keyframes stroke{to{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:shake 0.4s ease-out}@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}.countdown-container{margin-top:32px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .countdown-container{border-top-color:#333}.countdown-label{font-size:13px;color:#666;margin-bottom:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.05em}.dark .countdown-label{color:#999}#countdown-text{font-family:var(--mono-font);font-weight:600;color:#1a1a1a}.dark #countdown-text{color:#fafafa}.progress-track{width:100%;height:3px;background:#e5e5e5;border-radius:3px;overflow:hidden}.dark .progress-track{background:#333}.progress-bar{height:100%;background:linear-gradient(90deg,#10b981 0%,#059669 100%);border-radius:3px;transition:width 100ms linear;width:0}.minimal-actions{display:flex;gap:12px;margin-top:24px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .minimal-actions{border-top-color:#333}.minimal-button{flex:1;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer;transition:all 0.2s ease;font-family:var(--system-font);letter-spacing:-0.01em}.minimal-button:active{transform:scale(0.98)}.minimal-button.primary{background:#1a1a1a;color:white}.minimal-button.primary:hover{background:#000}.dark .minimal-button.primary{background:#fafafa;color:#1a1a1a}.dark .minimal-button.primary:hover{background:#fff}.minimal-button.secondary{background:#f5f5f5;color:#666}.minimal-button.secondary:hover{background:#e8e8e8}.dark .minimal-button.secondary{background:#2a2a2a;color:#999}.dark .minimal-button.secondary:hover{background:#333}.minimal-help{font-size:12px;color:#999;margin-top:16px;line-height:1.5}.dark .minimal-help{color:#666}.minimal-error-details{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;margin:20px 0;text-align:left;font-size:13px;line-height:1.6}.dark .minimal-error-details{background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.3)}.minimal-error-details strong{color:#dc2626;display:block;margin-bottom:4px;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:0.05em}.dark .minimal-error-details strong{color:#ef4444}.minimal-error-details code{font-family:var(--mono-font);background:rgba(0,0,0,0.05);padding:2px 6px;border-radius:4px;font-size:12px}.dark .minimal-error-details code{background:rgba(255,255,255,0.1)}@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important}}.minimal-button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (prefers-contrast:high){.minimal-card{border:2px solid currentColor}.minimal-button{border:1px solid currentColor}}</style><script> // Check for dark mode preference if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } // Auto-close window after 5 seconds const CLOSE_DELAY = 5000; let startTime = Date.now(); function updateCountdown() { const elapsed = Date.now() - startTime; const remaining = Math.max(0, CLOSE_DELAY - elapsed); const seconds = Math.ceil(remaining / 1000); const progress = Math.min(elapsed / CLOSE_DELAY, 1); const countdownEl = document.getElementById("countdown-text"); const progressEl = document.getElementById("progress-bar"); if (countdownEl && progressEl) { if (remaining > 0) { countdownEl.textContent = seconds + "s"; progressEl.style.width = progress * 100 + "%"; requestAnimationFrame(updateCountdown); } else { countdownEl.textContent = "Closing..."; setTimeout(() => window.close(), 100); } } } // Start when DOM is loaded window.addEventListener("DOMContentLoaded", () => { // Trigger check animation const checkmark = document.getElementById("checkmark"); if (checkmark) { setTimeout(() => checkmark.classList.add("animate"), 100); } // Start countdown requestAnimationFrame(updateCountdown); }); // Handle keyboard shortcuts document.addEventListener("keydown", (e) => { if (e.key === "Escape" || e.key === "Enter") { window.close(); } }); </script></head><body class="minimal-body"><div class="minimal-card"><div class="success-icon-container"><svg id="checkmark" class="checkmark" viewBox="0 0 52 52"><circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none" /><path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" /></svg></div><h1 class="minimal-title">Authorization Successful</h1><p class="minimal-subtitle">You can now return to your application</p><div class="countdown-container"><div class="countdown-label"> Closing in <span id="countdown-text">5s</span></div><div class="progress-track"><div id="progress-bar" class="progress-bar"></div></div></div></div></body></html>`;
9
9
 
10
- export const errorTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>OAuth - {{ERROR_TITLE}}</title><link rel="stylesheet" href="./styles.css" /></head><body><div class="container"><div class="icon error">{{ERROR_ICON}}</div><h1>{{ERROR_TITLE}}</h1><p>The authorization process could not be completed.</p> {{ERROR_DETAILS}} <p class="text-muted mt-24">You can close this window and try again.</p></div></body></html>`;
10
+ export const errorTemplate = `<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>{{ERROR_TITLE}}</title><style>:root{--system-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;--mono-font:ui-monospace,SFMono-Regular,"SF Mono",Consolas,"Liberation Mono",Menlo,monospace}@media (prefers-color-scheme:dark){:root{color-scheme:dark}}.minimal-body{margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:var(--system-font);background:linear-gradient(180deg,#fafafa 0%,#f0f0f0 100%);color:#1a1a1a;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dark .minimal-body{background:linear-gradient(180deg,#0a0a0a 0%,#1a1a1a 100%);color:#fafafa}.minimal-card{background:white;border-radius:16px;padding:48px 40px 40px;max-width:420px;width:90%;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.04),0 6px 16px rgba(0,0,0,0.08);animation:fadeUp 0.4s ease-out}.dark .minimal-card{background:#1f1f1f;box-shadow:0 1px 3px rgba(0,0,0,0.2),0 8px 24px rgba(0,0,0,0.4)}.minimal-title{font-size:24px;font-weight:600;margin:0 0 8px;letter-spacing:-0.02em;line-height:1.2}.minimal-subtitle{font-size:15px;color:#666;margin:0 0 32px;line-height:1.5}.dark .minimal-subtitle{color:#999}.success-icon-container{width:56px;height:56px;margin:0 auto 24px}.checkmark{width:56px;height:56px}.checkmark-circle{stroke:#10b981;stroke-width:2;stroke-miterlimit:10;fill:none}.checkmark-check{stroke:#10b981;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:48;stroke-dashoffset:48;fill:none}.checkmark.animate .checkmark-circle{animation:stroke 0.6s cubic-bezier(0.65,0,0.45,1) forwards}.checkmark.animate .checkmark-check{animation:stroke 0.3s cubic-bezier(0.65,0,0.45,1) 0.4s forwards}@keyframes stroke{to{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:shake 0.4s ease-out}@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}.countdown-container{margin-top:32px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .countdown-container{border-top-color:#333}.countdown-label{font-size:13px;color:#666;margin-bottom:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.05em}.dark .countdown-label{color:#999}#countdown-text{font-family:var(--mono-font);font-weight:600;color:#1a1a1a}.dark #countdown-text{color:#fafafa}.progress-track{width:100%;height:3px;background:#e5e5e5;border-radius:3px;overflow:hidden}.dark .progress-track{background:#333}.progress-bar{height:100%;background:linear-gradient(90deg,#10b981 0%,#059669 100%);border-radius:3px;transition:width 100ms linear;width:0}.minimal-actions{display:flex;gap:12px;margin-top:24px;padding-top:24px;border-top:1px solid #e5e5e5}.dark .minimal-actions{border-top-color:#333}.minimal-button{flex:1;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer;transition:all 0.2s ease;font-family:var(--system-font);letter-spacing:-0.01em}.minimal-button:active{transform:scale(0.98)}.minimal-button.primary{background:#1a1a1a;color:white}.minimal-button.primary:hover{background:#000}.dark .minimal-button.primary{background:#fafafa;color:#1a1a1a}.dark .minimal-button.primary:hover{background:#fff}.minimal-button.secondary{background:#f5f5f5;color:#666}.minimal-button.secondary:hover{background:#e8e8e8}.dark .minimal-button.secondary{background:#2a2a2a;color:#999}.dark .minimal-button.secondary:hover{background:#333}.minimal-help{font-size:12px;color:#999;margin-top:16px;line-height:1.5}.dark .minimal-help{color:#666}.minimal-error-details{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;margin:20px 0;text-align:left;font-size:13px;line-height:1.6}.dark .minimal-error-details{background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.3)}.minimal-error-details strong{color:#dc2626;display:block;margin-bottom:4px;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:0.05em}.dark .minimal-error-details strong{color:#ef4444}.minimal-error-details code{font-family:var(--mono-font);background:rgba(0,0,0,0.05);padding:2px 6px;border-radius:4px;font-size:12px}.dark .minimal-error-details code{background:rgba(255,255,255,0.1)}@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important}}.minimal-button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (prefers-contrast:high){.minimal-card{border:2px solid currentColor}.minimal-button{border:1px solid currentColor}}</style><script> // Check for dark mode preference if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } // Start animations when DOM is loaded window.addEventListener("DOMContentLoaded", () => { // Animate error icon const errorIcon = document.getElementById("error-icon"); if (errorIcon) { setTimeout(() => errorIcon.classList.add("animate"), 100); } }); // Handle keyboard shortcuts document.addEventListener("keydown", (e) => { if (e.key === "Escape") { window.close(); } else if (e.key === "Enter") { window.location.reload(); } }); </script></head><body class="minimal-body"><div class="minimal-card"><div class="error-icon-container" id="error-icon">{{ERROR_SVG_ICON}}</div><h1 class="minimal-title">{{ERROR_TITLE}}</h1><p class="minimal-subtitle">{{ERROR_MESSAGE}}</p> {{ERROR_DETAILS}} <div class="minimal-actions"><button onclick="window.location.reload()" class="minimal-button primary" > Try Again </button><button onclick="window.close()" class="minimal-button secondary"> Close </button></div><p class="minimal-help">{{HELP_TEXT}}</p></div></body></html>`;
11
11
 
12
12
  export function renderError(params: {
13
13
  error: string;
14
14
  error_description?: string;
15
15
  error_uri?: string;
16
16
  }): string {
17
- const errorTitle = params.error === 'access_denied' ? 'Access Denied' : 'Authorization Failed';
18
- const errorIcon = params.error === 'access_denied' ? '🚫' : '⚠️';
17
+ // Determine error title and message based on error type
18
+ let errorTitle = 'Authorization Failed';
19
+ let errorMessage = 'The authorization process could not be completed.';
20
+ let helpText = 'If the problem persists, please contact support.';
21
+ let errorSvgIcon = '';
22
+
23
+ switch (params.error) {
24
+ case 'access_denied':
25
+ errorTitle = 'Access Denied';
26
+ errorMessage = 'You have denied access to the application.';
27
+ helpText = 'Click "Try Again" to restart the authorization process.';
28
+ errorSvgIcon = `
29
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Access denied icon">
30
+ <path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
31
+ </svg>`;
32
+ break;
33
+ case 'invalid_request':
34
+ errorTitle = 'Invalid Request';
35
+ errorMessage = 'The authorization request was malformed or missing required parameters.';
36
+ errorSvgIcon = `
37
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Invalid request icon">
38
+ <path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
39
+ </svg>`;
40
+ break;
41
+ case 'unauthorized_client':
42
+ errorTitle = 'Unauthorized Client';
43
+ errorMessage = 'This application is not authorized to use this authentication method.';
44
+ errorSvgIcon = `
45
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Unauthorized icon">
46
+ <path d="M12 11V7a3 3 0 00-6 0v4m-1 0h8a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
47
+ </svg>`;
48
+ break;
49
+ case 'server_error':
50
+ errorTitle = 'Server Error';
51
+ errorMessage = 'The authorization server encountered an unexpected error.';
52
+ helpText = 'Please wait a moment and try again.';
53
+ errorSvgIcon = `
54
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Server error icon">
55
+ <path d="M12 9v6m0 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
56
+ </svg>`;
57
+ break;
58
+ default:
59
+ errorSvgIcon = `
60
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Warning icon">
61
+ <path d="M12 9v4m0 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
62
+ </svg>`;
63
+ }
19
64
 
20
65
  let errorDetails = '';
21
66
  if (params.error_description || params.error) {
22
67
  errorDetails = `
23
- <div class="error-details">
24
- <strong>Error Details:</strong>
68
+ <div class="minimal-error-details">
69
+ <strong>Error Code</strong>
25
70
  <code>${params.error}</code>
26
- ${params.error_description ? `<p class="mt-12">${params.error_description}</p>` : ''}
27
- ${params.error_uri ? `<p class="mt-8"><a href="${params.error_uri}" target="_blank">More information →</a></p>` : ''}
71
+ ${params.error_description ? `<p>${params.error_description}</p>` : ''}
72
+ ${params.error_uri ? `<p><a href="${params.error_uri}" target="_blank">More information →</a></p>` : ''}
28
73
  </div>`;
29
74
  }
30
75
 
31
76
  return errorTemplate
32
77
  .replace(/{{ERROR_TITLE}}/g, errorTitle)
33
- .replace('{{ERROR_ICON}}', errorIcon)
34
- .replace('{{ERROR_DETAILS}}', errorDetails);
78
+ .replace('{{ERROR_ICON}}', '⚠️') // Fallback for old template
79
+ .replace('{{ERROR_SVG_ICON}}', errorSvgIcon)
80
+ .replace('{{ERROR_MESSAGE}}', errorMessage)
81
+ .replace('{{ERROR_DETAILS}}', errorDetails)
82
+ .replace('{{HELP_TEXT}}', helpText);
35
83
  }
package/src/types.ts CHANGED
@@ -5,70 +5,70 @@
5
5
  * Configuration options for OAuth authorization code flow
6
6
  */
7
7
  export interface GetAuthCodeOptions {
8
- /**
8
+ /**
9
9
  * OAuth authorization URL that the user will be redirected to.
10
10
  * Should include all necessary query parameters like client_id, redirect_uri, etc.
11
11
  */
12
12
  authorizationUrl: string;
13
-
14
- /**
13
+
14
+ /**
15
15
  * Port for the local callback server. Make sure this matches the
16
16
  * redirect_uri registered with your OAuth provider.
17
- * @default 3000
17
+ * @default 3000
18
18
  */
19
19
  port?: number;
20
-
21
- /**
20
+
21
+ /**
22
22
  * Hostname to bind the server to. Should typically be "localhost" or "127.0.0.1"
23
23
  * for security reasons to prevent external access.
24
- * @default "localhost"
24
+ * @default "localhost"
25
25
  */
26
26
  hostname?: string;
27
-
28
- /**
27
+
28
+ /**
29
29
  * URL path for the OAuth callback. Must match the path in your registered
30
30
  * redirect_uri with the OAuth provider.
31
- * @default "/callback"
31
+ * @default "/callback"
32
32
  */
33
33
  callbackPath?: string;
34
-
35
- /**
34
+
35
+ /**
36
36
  * Timeout in milliseconds to wait for OAuth callback.
37
37
  * If no callback is received within this time, the operation will fail.
38
- * @default 30000
38
+ * @default 30000
39
39
  */
40
40
  timeout?: number;
41
-
42
- /**
41
+
42
+ /**
43
43
  * Whether to automatically open the authorization URL in the user's default browser.
44
44
  * Set to false for testing or when you want to handle browser opening manually.
45
- * @default true
45
+ * @default true
46
46
  */
47
47
  openBrowser?: boolean;
48
-
49
- /**
48
+
49
+ /**
50
50
  * Custom HTML content to display when authorization is successful.
51
51
  * If not provided, a default success page with auto-close functionality is used.
52
52
  */
53
53
  successHtml?: string;
54
-
55
- /**
54
+
55
+ /**
56
56
  * Custom HTML template to display when authorization fails.
57
57
  * Supports placeholders: {{error}}, {{error_description}}, {{error_uri}}
58
58
  * If not provided, a default error page is used.
59
59
  */
60
60
  errorHtml?: string;
61
-
62
- /**
61
+
62
+ /**
63
63
  * AbortSignal for cancellation support. Allows you to cancel the
64
64
  * OAuth flow programmatically (e.g., on user request or timeout).
65
65
  */
66
66
  signal?: AbortSignal;
67
-
68
- /**
67
+
68
+ /**
69
69
  * Callback function fired when any HTTP request is received by the server.
70
70
  * Useful for logging, debugging, or custom request handling.
71
71
  * @param req - The incoming HTTP request
72
72
  */
73
73
  onRequest?: (req: Request) => void;
74
- }
74
+ }