oauth-callback 1.2.0 → 1.2.1

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
@@ -413,6 +413,9 @@ bun test
413
413
  # Build
414
414
  bun run build
415
415
 
416
+ # Run documentation locally
417
+ bun run docs:dev # Start VitePress dev server at http://localhost:5173
418
+
416
419
  # Run examples
417
420
  bun run example:demo # Interactive demo
418
421
  bun run example:github # GitHub OAuth example
package/dist/index.js CHANGED
@@ -585,270 +585,129 @@ function renderError(params) {
585
585
 
586
586
  // src/server.ts
587
587
  function generateCallbackHTML(params, successHtml, errorHtml) {
588
- if (params.error) {
589
- if (errorHtml) {
590
- return errorHtml.replace(/{{error}}/g, params.error || "").replace(/{{error_description}}/g, params.error_description || "").replace(/{{error_uri}}/g, params.error_uri || "");
591
- }
592
- return renderError({
593
- error: params.error,
594
- error_description: params.error_description,
595
- error_uri: params.error_uri
596
- });
597
- }
598
- return successHtml || successTemplate;
588
+ if (!params.error)
589
+ return successHtml || successTemplate;
590
+ if (errorHtml)
591
+ return errorHtml.replace(/{{error}}/g, params.error || "").replace(/{{error_description}}/g, params.error_description || "").replace(/{{error_uri}}/g, params.error_uri || "");
592
+ return renderError({
593
+ error: params.error,
594
+ error_description: params.error_description,
595
+ error_uri: params.error_uri
596
+ });
599
597
  }
600
598
 
601
- class BunCallbackServer {
602
- server;
603
- callbackPromise;
604
- callbackPath = "/callback";
599
+ class BaseCallbackServer {
600
+ callbackListeners = new Map;
605
601
  successHtml;
606
602
  errorHtml;
607
603
  onRequest;
608
604
  abortHandler;
609
- async start(options) {
610
- const {
611
- port,
612
- hostname = "localhost",
613
- successHtml,
614
- errorHtml,
615
- signal,
616
- onRequest
617
- } = options;
605
+ signal;
606
+ setup(options) {
607
+ const { successHtml, errorHtml, signal, onRequest } = options;
618
608
  this.successHtml = successHtml;
619
609
  this.errorHtml = errorHtml;
620
610
  this.onRequest = onRequest;
621
- if (signal) {
622
- if (signal.aborted) {
623
- throw new Error("Operation aborted");
624
- }
625
- this.abortHandler = () => {
626
- this.stop();
627
- if (this.callbackPromise) {
628
- this.callbackPromise.reject(new Error("Operation aborted"));
629
- }
630
- };
631
- signal.addEventListener("abort", this.abortHandler);
632
- }
633
- this.server = Bun.serve({
634
- port,
635
- hostname,
636
- fetch: (request) => this.handleRequest(request)
637
- });
611
+ this.signal = signal;
612
+ if (!signal)
613
+ return;
614
+ if (signal.aborted)
615
+ throw new Error("Operation aborted");
616
+ this.abortHandler = () => this.stop();
617
+ signal.addEventListener("abort", this.abortHandler);
638
618
  }
639
619
  handleRequest(request) {
640
- if (this.onRequest) {
641
- this.onRequest(request);
642
- }
620
+ this.onRequest?.(request);
643
621
  const url = new URL(request.url);
644
- if (url.pathname === this.callbackPath) {
645
- const params = {};
646
- for (const [key, value] of url.searchParams) {
647
- params[key] = value;
648
- }
649
- if (this.callbackPromise) {
650
- this.callbackPromise.resolve(params);
651
- }
652
- return new Response(generateCallbackHTML(params, this.successHtml, this.errorHtml), {
653
- status: 200,
654
- headers: { "Content-Type": "text/html" }
655
- });
656
- }
657
- return new Response("Not Found", { status: 404 });
622
+ const listener = this.callbackListeners.get(url.pathname);
623
+ if (!listener)
624
+ return new Response("Not Found", { status: 404 });
625
+ const params = {};
626
+ for (const [key, value] of url.searchParams)
627
+ params[key] = value;
628
+ listener.resolve(params);
629
+ return new Response(generateCallbackHTML(params, this.successHtml, this.errorHtml), {
630
+ status: 200,
631
+ headers: { "Content-Type": "text/html" }
632
+ });
658
633
  }
659
634
  async waitForCallback(path2, timeout) {
660
- this.callbackPath = path2;
661
- return new Promise((resolve, reject) => {
662
- let isResolved = false;
663
- const timer = setTimeout(() => {
664
- if (!isResolved) {
665
- isResolved = true;
666
- this.callbackPromise = undefined;
667
- reject(new Error(`OAuth callback timeout after ${timeout}ms waiting for ${path2}`));
668
- }
669
- }, timeout);
670
- const wrappedResolve = (result) => {
671
- if (!isResolved) {
672
- isResolved = true;
673
- clearTimeout(timer);
674
- this.callbackPromise = undefined;
675
- resolve(result);
676
- }
677
- };
678
- const wrappedReject = (error) => {
679
- if (!isResolved) {
680
- isResolved = true;
681
- clearTimeout(timer);
682
- this.callbackPromise = undefined;
683
- reject(error);
684
- }
685
- };
686
- this.callbackPromise = { resolve: wrappedResolve, reject: wrappedReject };
687
- });
635
+ if (this.callbackListeners.has(path2))
636
+ return Promise.reject(new Error(`A listener for the path "${path2}" is already active.`));
637
+ try {
638
+ return await Promise.race([
639
+ new Promise((resolve, reject) => {
640
+ this.callbackListeners.set(path2, { resolve, reject });
641
+ }),
642
+ new Promise((_, reject) => {
643
+ setTimeout(() => {
644
+ reject(new Error(`OAuth callback timeout after ${timeout}ms waiting for ${path2}`));
645
+ }, timeout);
646
+ })
647
+ ]);
648
+ } finally {
649
+ this.callbackListeners.delete(path2);
650
+ }
688
651
  }
689
652
  async stop() {
690
- if (this.abortHandler) {
691
- const signal = this.server?.signal;
692
- if (signal) {
693
- signal.removeEventListener("abort", this.abortHandler);
694
- }
653
+ if (this.abortHandler && this.signal) {
654
+ this.signal.removeEventListener("abort", this.abortHandler);
695
655
  this.abortHandler = undefined;
696
656
  }
697
- if (this.callbackPromise) {
698
- this.callbackPromise.reject(new Error("Server stopped before callback received"));
699
- this.callbackPromise = undefined;
700
- }
701
- if (this.server) {
702
- this.server.stop();
703
- this.server = undefined;
704
- }
657
+ for (const listener of this.callbackListeners.values())
658
+ listener.reject(new Error("Server stopped before callback received"));
659
+ this.callbackListeners.clear();
660
+ await this.stopServer();
705
661
  }
706
662
  }
707
663
 
708
- class DenoCallbackServer {
664
+ class BunCallbackServer extends BaseCallbackServer {
709
665
  server;
710
- callbackPromise;
711
- callbackPath = "/callback";
712
- abortController;
713
- successHtml;
714
- errorHtml;
715
- onRequest;
716
- abortHandler;
717
666
  async start(options) {
718
- const {
667
+ this.setup(options);
668
+ const { port, hostname = "localhost" } = options;
669
+ this.server = Bun.serve({
719
670
  port,
720
- hostname = "localhost",
721
- successHtml,
722
- errorHtml,
723
- signal,
724
- onRequest
725
- } = options;
726
- this.successHtml = successHtml;
727
- this.errorHtml = errorHtml;
728
- this.onRequest = onRequest;
729
- this.abortController = new AbortController;
730
- if (signal) {
731
- if (signal.aborted) {
732
- throw new Error("Operation aborted");
733
- }
734
- this.abortHandler = () => {
735
- this.abortController?.abort();
736
- if (this.callbackPromise) {
737
- this.callbackPromise.reject(new Error("Operation aborted"));
738
- }
739
- };
740
- signal.addEventListener("abort", this.abortHandler);
741
- }
742
- this.server = Deno.serve({ port, hostname, signal: this.abortController.signal }, (request) => this.handleRequest(request));
743
- }
744
- handleRequest(request) {
745
- if (this.onRequest) {
746
- this.onRequest(request);
747
- }
748
- const url = new URL(request.url);
749
- if (url.pathname === this.callbackPath) {
750
- const params = {};
751
- for (const [key, value] of url.searchParams) {
752
- params[key] = value;
753
- }
754
- if (this.callbackPromise) {
755
- this.callbackPromise.resolve(params);
756
- }
757
- return new Response(generateCallbackHTML(params, this.successHtml, this.errorHtml), {
758
- status: 200,
759
- headers: { "Content-Type": "text/html" }
760
- });
761
- }
762
- return new Response("Not Found", { status: 404 });
763
- }
764
- async waitForCallback(path2, timeout) {
765
- this.callbackPath = path2;
766
- return new Promise((resolve, reject) => {
767
- let isResolved = false;
768
- const timer = setTimeout(() => {
769
- if (!isResolved) {
770
- isResolved = true;
771
- this.callbackPromise = undefined;
772
- reject(new Error(`OAuth callback timeout after ${timeout}ms waiting for ${path2}`));
773
- }
774
- }, timeout);
775
- const wrappedResolve = (result) => {
776
- if (!isResolved) {
777
- isResolved = true;
778
- clearTimeout(timer);
779
- this.callbackPromise = undefined;
780
- resolve(result);
781
- }
782
- };
783
- const wrappedReject = (error) => {
784
- if (!isResolved) {
785
- isResolved = true;
786
- clearTimeout(timer);
787
- this.callbackPromise = undefined;
788
- reject(error);
789
- }
790
- };
791
- this.callbackPromise = { resolve: wrappedResolve, reject: wrappedReject };
671
+ hostname,
672
+ fetch: (request) => this.handleRequest(request)
792
673
  });
793
674
  }
794
- async stop() {
795
- if (this.abortHandler) {
796
- const signal = this.server?.signal;
797
- if (signal) {
798
- signal.removeEventListener("abort", this.abortHandler);
799
- }
800
- this.abortHandler = undefined;
801
- }
802
- if (this.callbackPromise) {
803
- this.callbackPromise.reject(new Error("Server stopped before callback received"));
804
- this.callbackPromise = undefined;
805
- }
806
- if (this.abortController) {
807
- this.abortController.abort();
808
- this.abortController = undefined;
809
- }
675
+ async stopServer() {
676
+ if (!this.server)
677
+ return;
678
+ this.server.stop();
810
679
  this.server = undefined;
811
680
  }
812
681
  }
813
682
 
814
- class NodeCallbackServer {
683
+ class DenoCallbackServer extends BaseCallbackServer {
684
+ abortController;
685
+ async start(options) {
686
+ this.setup(options);
687
+ const { port, hostname = "localhost" } = options;
688
+ this.abortController = new AbortController;
689
+ options.signal?.addEventListener("abort", () => this.abortController?.abort());
690
+ Deno.serve({ port, hostname, signal: this.abortController.signal }, (request) => this.handleRequest(request));
691
+ }
692
+ async stopServer() {
693
+ if (!this.abortController)
694
+ return;
695
+ this.abortController.abort();
696
+ this.abortController = undefined;
697
+ }
698
+ }
699
+
700
+ class NodeCallbackServer extends BaseCallbackServer {
815
701
  server;
816
- callbackPromise;
817
- callbackPath = "/callback";
818
- successHtml;
819
- errorHtml;
820
- onRequest;
821
- abortHandler;
822
702
  async start(options) {
823
- const {
824
- port,
825
- hostname = "localhost",
826
- successHtml,
827
- errorHtml,
828
- signal,
829
- onRequest
830
- } = options;
831
- this.successHtml = successHtml;
832
- this.errorHtml = errorHtml;
833
- this.onRequest = onRequest;
834
- if (signal) {
835
- if (signal.aborted) {
836
- throw new Error("Operation aborted");
837
- }
838
- this.abortHandler = () => {
839
- this.stop();
840
- if (this.callbackPromise) {
841
- this.callbackPromise.reject(new Error("Operation aborted"));
842
- }
843
- };
844
- signal.addEventListener("abort", this.abortHandler);
845
- }
703
+ this.setup(options);
704
+ const { port, hostname = "localhost" } = options;
846
705
  const { createServer } = await import("node:http");
847
706
  return new Promise((resolve, reject) => {
848
707
  this.server = createServer(async (req, res) => {
849
708
  try {
850
- const request = this.nodeToWebRequest(req, port);
851
- const response = await this.handleRequest(request);
709
+ const request = this.nodeToWebRequest(req, port, hostname);
710
+ const response = this.handleRequest(request);
852
711
  res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
853
712
  const body = await response.text();
854
713
  res.end(body);
@@ -857,102 +716,43 @@ class NodeCallbackServer {
857
716
  res.end("Internal Server Error");
858
717
  }
859
718
  });
719
+ if (options.signal)
720
+ options.signal.addEventListener("abort", () => this.server?.close());
860
721
  this.server.listen(port, hostname, () => resolve());
861
722
  this.server.on("error", reject);
862
723
  });
863
724
  }
864
- nodeToWebRequest(req, port) {
865
- const url = new URL(req.url, `http://localhost:${port}`);
725
+ async stopServer() {
726
+ if (!this.server)
727
+ return;
728
+ return new Promise((resolve) => {
729
+ this.server?.close(() => {
730
+ this.server = undefined;
731
+ resolve();
732
+ });
733
+ });
734
+ }
735
+ nodeToWebRequest(req, port, hostname) {
736
+ const host = req.headers.host || `${hostname}:${port}`;
737
+ const url = new URL(req.url, `http://${host}`);
866
738
  const headers = new Headers;
867
739
  for (const [key, value] of Object.entries(req.headers)) {
868
- if (typeof value === "string") {
740
+ if (typeof value === "string")
869
741
  headers.set(key, value);
870
- } else if (Array.isArray(value)) {
742
+ else if (Array.isArray(value))
871
743
  headers.set(key, value.join(", "));
872
- }
873
744
  }
874
745
  return new Request(url.toString(), {
875
746
  method: req.method,
876
747
  headers
877
748
  });
878
749
  }
879
- async handleRequest(request) {
880
- if (this.onRequest) {
881
- this.onRequest(request);
882
- }
883
- const url = new URL(request.url);
884
- if (url.pathname === this.callbackPath) {
885
- const params = {};
886
- for (const [key, value] of url.searchParams) {
887
- params[key] = value;
888
- }
889
- if (this.callbackPromise) {
890
- this.callbackPromise.resolve(params);
891
- }
892
- return new Response(generateCallbackHTML(params, this.successHtml, this.errorHtml), {
893
- status: 200,
894
- headers: { "Content-Type": "text/html" }
895
- });
896
- }
897
- return new Response("Not Found", { status: 404 });
898
- }
899
- async waitForCallback(path2, timeout) {
900
- this.callbackPath = path2;
901
- return new Promise((resolve, reject) => {
902
- let isResolved = false;
903
- const timer = setTimeout(() => {
904
- if (!isResolved) {
905
- isResolved = true;
906
- this.callbackPromise = undefined;
907
- reject(new Error(`OAuth callback timeout after ${timeout}ms waiting for ${path2}`));
908
- }
909
- }, timeout);
910
- const wrappedResolve = (result) => {
911
- if (!isResolved) {
912
- isResolved = true;
913
- clearTimeout(timer);
914
- this.callbackPromise = undefined;
915
- resolve(result);
916
- }
917
- };
918
- const wrappedReject = (error) => {
919
- if (!isResolved) {
920
- isResolved = true;
921
- clearTimeout(timer);
922
- this.callbackPromise = undefined;
923
- reject(error);
924
- }
925
- };
926
- this.callbackPromise = { resolve: wrappedResolve, reject: wrappedReject };
927
- });
928
- }
929
- async stop() {
930
- if (this.abortHandler) {
931
- const signal = this.server?.signal;
932
- if (signal) {
933
- signal.removeEventListener("abort", this.abortHandler);
934
- }
935
- this.abortHandler = undefined;
936
- }
937
- if (this.callbackPromise) {
938
- this.callbackPromise.reject(new Error("Server stopped before callback received"));
939
- this.callbackPromise = undefined;
940
- }
941
- if (this.server) {
942
- return new Promise((resolve) => {
943
- this.server.close(() => resolve());
944
- this.server = undefined;
945
- });
946
- }
947
- }
948
750
  }
949
751
  function createCallbackServer() {
950
- if (typeof Bun !== "undefined") {
752
+ if (typeof Bun !== "undefined")
951
753
  return new BunCallbackServer;
952
- }
953
- if (typeof Deno !== "undefined") {
754
+ if (typeof Deno !== "undefined")
954
755
  return new DenoCallbackServer;
955
- }
956
756
  return new NodeCallbackServer;
957
757
  }
958
758
  // src/storage/memory.ts