oauth-callback 0.1.1 → 1.0.0

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
@@ -7,15 +7,19 @@
7
7
 
8
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.
9
9
 
10
+ <div align="center">
11
+ <img src="https://raw.githubusercontent.com/kriasoft/oauth-callback/main/examples/notion.gif" alt="OAuth Callback Demo" width="100%" style="max-width: 800px; height: auto;">
12
+ </div>
13
+
10
14
  ## Features
11
15
 
12
16
  - 🚀 **Multi-runtime support** - Works with Node.js 18+, Deno, and Bun
13
- - 🔒 **Secure localhost-only server** for OAuth callbacks
17
+ - 🔒 **Secure localhost-only server** for OAuth callbacks
14
18
  - ⚡ **Minimal dependencies** - Only requires `open` package
15
19
  - 🎯 **TypeScript support** out of the box
16
20
  - 🛡️ **Comprehensive OAuth error handling** with detailed error classes
17
21
  - 🔄 **Automatic server cleanup** after callback
18
- - 🎪 **Auto-closing success pages** with countdown timer
22
+ - 🎪 **Clean success pages** with animated checkmark
19
23
  - 🎨 **Customizable HTML templates** with placeholder support
20
24
  - 🚦 **AbortSignal support** for programmatic cancellation
21
25
  - 📝 **Request logging and debugging** callbacks
@@ -217,6 +221,66 @@ class OAuthError extends Error {
217
221
  - No data is stored persistently
218
222
  - State parameter validation should be implemented by the application
219
223
 
224
+ ## Running the Examples
225
+
226
+ ### Interactive Demo (No Setup Required)
227
+
228
+ Try the library instantly with the built-in demo that includes a mock OAuth server:
229
+
230
+ ```bash
231
+ # Run the demo - no credentials needed!
232
+ bun run example:demo
233
+
234
+ # Run without opening browser (for CI/testing)
235
+ bun run examples/demo.ts --no-browser
236
+ ```
237
+
238
+ The demo showcases:
239
+
240
+ - Dynamic client registration (simplified OAuth 2.0 DCR)
241
+ - Complete authorization flow with mock provider
242
+ - Multiple scenarios (success, access denied, invalid scope)
243
+ - Custom HTML templates for success/error pages
244
+ - Token exchange and API usage simulation
245
+
246
+ ### Real OAuth Examples
247
+
248
+ #### GitHub OAuth
249
+
250
+ For testing with GitHub OAuth:
251
+
252
+ ```bash
253
+ # Set up GitHub OAuth App credentials
254
+ export GITHUB_CLIENT_ID="your_client_id"
255
+ export GITHUB_CLIENT_SECRET="your_client_secret"
256
+
257
+ # Run the GitHub example
258
+ bun run example:github
259
+ ```
260
+
261
+ This example demonstrates:
262
+
263
+ - Setting up OAuth with GitHub
264
+ - Handling the authorization callback
265
+ - Exchanging the code for an access token
266
+ - Using the token to fetch user information
267
+
268
+ #### Notion MCP with Dynamic Client Registration
269
+
270
+ For testing with Notion's Model Context Protocol server:
271
+
272
+ ```bash
273
+ # No credentials needed - uses Dynamic Client Registration!
274
+ bun run example:notion
275
+ ```
276
+
277
+ This example demonstrates:
278
+
279
+ - Dynamic Client Registration (OAuth 2.0 DCR) - no pre-configured client ID/secret needed
280
+ - Integration with Model Context Protocol (MCP) servers
281
+ - Automatic client registration with the authorization server
282
+ - Using the MCP SDK's OAuth capabilities
283
+
220
284
  ## Development
221
285
 
222
286
  ```bash
@@ -227,10 +291,12 @@ bun install
227
291
  bun test
228
292
 
229
293
  # Build
230
- bun build ./src/index.ts --outdir ./dist
294
+ bun run build
231
295
 
232
- # Run example
233
- bun run example.ts
296
+ # Run examples
297
+ bun run example:demo # Interactive demo
298
+ bun run example:github # GitHub OAuth example
299
+ bun run example:notion # Notion MCP example with Dynamic Client Registration
234
300
  ```
235
301
 
236
302
  ## Requirements
@@ -271,6 +337,17 @@ Contributions are welcome! Please feel free to submit a Pull Request.
271
337
 
272
338
  This project is released under the MIT License. Feel free to use it in your projects, modify it to suit your needs, and share it with others. We believe in open source and hope this tool makes OAuth integration easier for everyone!
273
339
 
340
+ ## Related Projects
341
+
342
+ - [**MCP Client Generator**](https://github.com/kriasoft/mcp-client-gen) - Generate TypeScript clients from MCP server specifications. Perfect companion for building MCP-enabled applications with OAuth support ([npm](https://www.npmjs.com/package/mcp-client-gen)).
343
+ - [**React Starter Kit**](https://github.com/kriasoft/react-starter-kit) - Full-stack React application template with authentication, including OAuth integration examples.
344
+
345
+ ## Backers
346
+
347
+ Support this project by becoming a backer. Your logo will show up here with a link to your website.
348
+
349
+ <a href="https://reactstarter.com/b/1"><img src="https://reactstarter.com/b/1.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/2"><img src="https://reactstarter.com/b/2.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/3"><img src="https://reactstarter.com/b/3.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/4"><img src="https://reactstarter.com/b/4.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/5"><img src="https://reactstarter.com/b/5.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/6"><img src="https://reactstarter.com/b/6.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/7"><img src="https://reactstarter.com/b/7.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/8"><img src="https://reactstarter.com/b/8.png" height="60" /></a>
350
+
274
351
  ## Support
275
352
 
276
353
  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;stroke-dasharray:157;stroke-dashoffset:157;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{100%{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:errorPulse 0.5s ease-out,shake 0.4s ease-out 0.3s}.error-icon-container.animate svg circle{stroke-dasharray:63;stroke-dashoffset:63;animation:errorStroke 0.5s cubic-bezier(0.65,0,0.45,1) forwards}.error-icon-container.animate svg path{stroke-dasharray:20;stroke-dashoffset:20;animation:errorStroke 0.4s cubic-bezier(0.65,0,0.45,1) 0.3s forwards}@keyframes errorPulse{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}@keyframes errorStroke{to{stroke-dashoffset:0}}@keyframes shake{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-4px) scale(1)}75%{transform:translateX(4px) scale(1)}}.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> if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } window.addEventListener("DOMContentLoaded", () => { const checkmark = document.getElementById("checkmark"); if (checkmark) { setTimeout(() => checkmark.classList.add("animate"), 100); } });</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></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;stroke-dasharray:157;stroke-dashoffset:157;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{100%{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:errorPulse 0.5s ease-out,shake 0.4s ease-out 0.3s}.error-icon-container.animate svg circle{stroke-dasharray:63;stroke-dashoffset:63;animation:errorStroke 0.5s cubic-bezier(0.65,0,0.45,1) forwards}.error-icon-container.animate svg path{stroke-dasharray:20;stroke-dashoffset:20;animation:errorStroke 0.4s cubic-bezier(0.65,0,0.45,1) 0.3s forwards}@keyframes errorPulse{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}@keyframes errorStroke{to{stroke-dashoffset:0}}@keyframes shake{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-4px) scale(1)}75%{transform:translateX(4px) scale(1)}}.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> if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } window.addEventListener("DOMContentLoaded", () => { const errorIcon = document.getElementById("error-icon"); if (errorIcon) { setTimeout(() => errorIcon.classList.add("animate"), 100); } }); 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;stroke-dasharray:157;stroke-dashoffset:157;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{100%{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:errorPulse 0.5s ease-out,shake 0.4s ease-out 0.3s}.error-icon-container.animate svg circle{stroke-dasharray:63;stroke-dashoffset:63;animation:errorStroke 0.5s cubic-bezier(0.65,0,0.45,1) forwards}.error-icon-container.animate svg path{stroke-dasharray:20;stroke-dashoffset:20;animation:errorStroke 0.4s cubic-bezier(0.65,0,0.45,1) 0.3s forwards}@keyframes errorPulse{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}@keyframes errorStroke{to{stroke-dashoffset:0}}@keyframes shake{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-4px) scale(1)}75%{transform:translateX(4px) scale(1)}}.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> if (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) { document.documentElement.classList.add(\"dark\"); } window.addEventListener(\"DOMContentLoaded\", () => { const checkmark = document.getElementById(\"checkmark\"); if (checkmark) { setTimeout(() => checkmark.classList.add(\"animate\"), 100); } });</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></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;stroke-dasharray:157;stroke-dashoffset:157;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{100%{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:errorPulse 0.5s ease-out,shake 0.4s ease-out 0.3s}.error-icon-container.animate svg circle{stroke-dasharray:63;stroke-dashoffset:63;animation:errorStroke 0.5s cubic-bezier(0.65,0,0.45,1) forwards}.error-icon-container.animate svg path{stroke-dasharray:20;stroke-dashoffset:20;animation:errorStroke 0.4s cubic-bezier(0.65,0,0.45,1) 0.3s forwards}@keyframes errorPulse{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}@keyframes errorStroke{to{stroke-dashoffset:0}}@keyframes shake{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-4px) scale(1)}75%{transform:translateX(4px) scale(1)}}.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> if (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) { document.documentElement.classList.add(\"dark\"); } window.addEventListener(\"DOMContentLoaded\", () => { const errorIcon = document.getElementById(\"error-icon\"); if (errorIcon) { setTimeout(() => errorIcon.classList.add(\"animate\"), 100); } }); 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,uuMAA6qM,CAAC;AAE1sM,eAAO,MAAM,aAAa,4+MAAo7M,CAAC;AAE/8M,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,CAsET"}
package/package.json CHANGED
@@ -1,37 +1,39 @@
1
1
  {
2
2
  "name": "oauth-callback",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "description": "Simple local OAuth callback server for capturing authorization codes in CLI tools and desktop apps",
5
5
  "keywords": [
6
6
  "oauth",
7
+ "oauth2",
7
8
  "callback",
9
+ "authorization",
8
10
  "authentication",
9
11
  "auth",
10
- "bun",
11
- "oauth2",
12
- "login",
13
- "authorization",
14
- "web",
15
- "handler",
16
- "security",
17
- "redirect",
18
12
  "token",
19
- "javascript",
20
- "typescript",
21
- "rfc6749",
22
- "rfc7591",
13
+ "access-token",
14
+ "refresh-token",
15
+ "redirect",
16
+ "login",
23
17
  "openid",
24
18
  "oidc",
19
+ "mcp",
20
+ "typescript",
21
+ "javascript",
22
+ "bun",
25
23
  "client",
26
24
  "server",
25
+ "handler",
26
+ "security",
27
+ "rfc6749",
28
+ "rfc7591",
29
+ "jwt",
30
+ "bearer",
31
+ "mcp-client",
32
+ "web",
27
33
  "middleware",
28
34
  "express",
29
35
  "fastify",
30
- "session",
31
- "jwt",
32
- "bearer",
33
- "access-token",
34
- "refresh-token"
36
+ "session"
35
37
  ],
36
38
  "author": "Konstantin Tarkus <koistya@kriasoft.com>",
37
39
  "license": "MIT",
@@ -72,9 +74,10 @@
72
74
  "typescript": "^5"
73
75
  },
74
76
  "devDependencies": {
77
+ "@modelcontextprotocol/sdk": "^1.17.3",
75
78
  "@types/bun": "latest",
76
- "typescript": "^5.9.2",
77
- "prettier": "^3.6.2"
79
+ "prettier": "^3.6.2",
80
+ "typescript": "^5.9.2"
78
81
  },
79
82
  "prettier": {
80
83
  "printWidth": 80,
@@ -87,6 +90,9 @@
87
90
  "build:templates": "bun run templates/build.ts",
88
91
  "build": "bun run build:templates && bun build ./src/index.ts --outdir=./dist --target=node && tsc --declaration --emitDeclarationOnly --outDir ./dist",
89
92
  "prepublishOnly": "bun run build",
90
- "test": "bun test"
93
+ "test": "bun test",
94
+ "example:demo": "bun run examples/demo.ts",
95
+ "example:github": "bun run examples/github.ts",
96
+ "example:notion": "bun run examples/notion.ts"
91
97
  }
92
98
  }
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,86 @@
1
1
  /**
2
2
  * Auto-generated OAuth callback HTML templates
3
- * Generated from assets/*.html and assets/*.css
4
- *
5
- * To regenerate: bun run assets/build.ts
3
+ * Generated from templates/*.html and templates/*.css
4
+ *
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;stroke-dasharray:157;stroke-dashoffset:157;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{100%{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:errorPulse 0.5s ease-out,shake 0.4s ease-out 0.3s}.error-icon-container.animate svg circle{stroke-dasharray:63;stroke-dashoffset:63;animation:errorStroke 0.5s cubic-bezier(0.65,0,0.45,1) forwards}.error-icon-container.animate svg path{stroke-dasharray:20;stroke-dashoffset:20;animation:errorStroke 0.4s cubic-bezier(0.65,0,0.45,1) 0.3s forwards}@keyframes errorPulse{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}@keyframes errorStroke{to{stroke-dashoffset:0}}@keyframes shake{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-4px) scale(1)}75%{transform:translateX(4px) scale(1)}}.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> if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } window.addEventListener("DOMContentLoaded", () => { const checkmark = document.getElementById("checkmark"); if (checkmark) { setTimeout(() => checkmark.classList.add("animate"), 100); } });</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></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;stroke-dasharray:157;stroke-dashoffset:157;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{100%{stroke-dashoffset:0}}.error-icon-container{width:56px;height:56px;margin:0 auto 24px;color:#ef4444}.error-icon-container.animate svg{animation:errorPulse 0.5s ease-out,shake 0.4s ease-out 0.3s}.error-icon-container.animate svg circle{stroke-dasharray:63;stroke-dashoffset:63;animation:errorStroke 0.5s cubic-bezier(0.65,0,0.45,1) forwards}.error-icon-container.animate svg path{stroke-dasharray:20;stroke-dashoffset:20;animation:errorStroke 0.4s cubic-bezier(0.65,0,0.45,1) 0.3s forwards}@keyframes errorPulse{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}@keyframes errorStroke{to{stroke-dashoffset:0}}@keyframes shake{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-4px) scale(1)}75%{transform:translateX(4px) scale(1)}}.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> if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } window.addEventListener("DOMContentLoaded", () => { const errorIcon = document.getElementById("error-icon"); if (errorIcon) { setTimeout(() => errorIcon.classList.add("animate"), 100); } }); 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' ? '🚫' : '⚠️';
19
-
20
- let errorDetails = '';
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 =
36
+ "The authorization request was malformed or missing required parameters.";
37
+ errorSvgIcon = `
38
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Invalid request icon">
39
+ <path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
40
+ </svg>`;
41
+ break;
42
+ case "unauthorized_client":
43
+ errorTitle = "Unauthorized Client";
44
+ errorMessage =
45
+ "This application is not authorized to use this authentication method.";
46
+ errorSvgIcon = `
47
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Unauthorized icon">
48
+ <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"/>
49
+ </svg>`;
50
+ break;
51
+ case "server_error":
52
+ errorTitle = "Server Error";
53
+ errorMessage =
54
+ "The authorization server encountered an unexpected error.";
55
+ helpText = "Please wait a moment and try again.";
56
+ errorSvgIcon = `
57
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Server error icon">
58
+ <path d="M12 9v6m0 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
59
+ </svg>`;
60
+ break;
61
+ default:
62
+ errorSvgIcon = `
63
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Warning icon">
64
+ <path d="M12 9v4m0 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
65
+ </svg>`;
66
+ }
67
+
68
+ let errorDetails = "";
21
69
  if (params.error_description || params.error) {
22
70
  errorDetails = `
23
- <div class="error-details">
24
- <strong>Error Details:</strong>
71
+ <div class="minimal-error-details">
72
+ <strong>Error Code</strong>
25
73
  <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>` : ''}
74
+ ${params.error_description ? `<p>${params.error_description}</p>` : ""}
75
+ ${params.error_uri ? `<p><a href="${params.error_uri}" target="_blank">More information →</a></p>` : ""}
28
76
  </div>`;
29
77
  }
30
78
 
31
79
  return errorTemplate
32
80
  .replace(/{{ERROR_TITLE}}/g, errorTitle)
33
- .replace('{{ERROR_ICON}}', errorIcon)
34
- .replace('{{ERROR_DETAILS}}', errorDetails);
81
+ .replace("{{ERROR_ICON}}", "⚠️") // Fallback for old template
82
+ .replace("{{ERROR_SVG_ICON}}", errorSvgIcon)
83
+ .replace("{{ERROR_MESSAGE}}", errorMessage)
84
+ .replace("{{ERROR_DETAILS}}", errorDetails)
85
+ .replace("{{HELP_TEXT}}", helpText);
35
86
  }
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
+ }