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 +54 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -12
- package/dist/server.d.ts.map +1 -1
- package/dist/templates.d.ts +4 -4
- package/dist/templates.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/errors.ts +3 -3
- package/src/index.ts +7 -8
- package/src/server.ts +92 -40
- package/src/templates.ts +60 -12
- package/src/types.ts +25 -25
package/README.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
# OAuth Callback
|
|
1
|
+
# OAuth Callback
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/oauth-callback)
|
|
4
|
+
[](https://npmjs.com/package/oauth-callback)
|
|
5
|
+
[](https://github.com/kriasoft/oauth-callback/blob/main/LICENSE)
|
|
6
|
+
[](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
|
|
272
|
+
bun run build
|
|
226
273
|
|
|
227
|
-
# Run
|
|
228
|
-
bun run example
|
|
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/
|
|
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!
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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>
|
|
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>
|
|
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
|
-
|
|
520
|
-
|
|
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
|
|
567
|
+
<div class="minimal-error-details">
|
|
568
|
+
<strong>Error Code</strong>
|
|
526
569
|
<code>${params.error}</code>
|
|
527
|
-
${params.error_description ? `<p
|
|
528
|
-
${params.error_uri ? `<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}}",
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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;
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/templates.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated OAuth callback HTML templates
|
|
3
|
-
* Generated from
|
|
3
|
+
* Generated from templates/*.html and templates/*.css
|
|
4
4
|
*
|
|
5
|
-
* To regenerate: bun run
|
|
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>
|
|
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>
|
|
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;
|
package/dist/templates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,eAAe,
|
|
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.
|
|
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
|
-
"
|
|
77
|
-
"
|
|
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 =
|
|
55
|
-
? { authorizationUrl: input }
|
|
56
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
287
|
-
|
|
288
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
470
|
-
|
|
471
|
-
|
|
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(
|
|
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(
|
|
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
|
|
3
|
+
* Generated from templates/*.html and templates/*.css
|
|
4
4
|
*
|
|
5
|
-
* To regenerate: bun run
|
|
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>
|
|
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>
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
68
|
+
<div class="minimal-error-details">
|
|
69
|
+
<strong>Error Code</strong>
|
|
25
70
|
<code>${params.error}</code>
|
|
26
|
-
${params.error_description ? `<p
|
|
27
|
-
${params.error_uri ? `<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}}',
|
|
34
|
-
.replace('{{
|
|
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
|
+
}
|