@fluixi/oauth2 1.0.0-alpha.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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api-guard.cjs +185 -0
  3. package/dist/api-guard.d.ts +94 -0
  4. package/dist/api-guard.d.ts.map +1 -0
  5. package/dist/api-guard.js +127 -0
  6. package/dist/api-guard.mjs +141 -0
  7. package/dist/browser-FDUOEUM2.mjs +3544 -0
  8. package/dist/browser-ONMHX3Z3.mjs +3545 -0
  9. package/dist/browser.cjs +735 -0
  10. package/dist/browser.d.ts +3 -0
  11. package/dist/browser.d.ts.map +1 -0
  12. package/dist/browser.js +448 -0
  13. package/dist/browser.mjs +704 -0
  14. package/dist/callback.cjs +125 -0
  15. package/dist/callback.d.ts +113 -0
  16. package/dist/callback.d.ts.map +1 -0
  17. package/dist/callback.js +183 -0
  18. package/dist/callback.mjs +106 -0
  19. package/dist/chunk-25WE6UWI.mjs +10 -0
  20. package/dist/chunk-4NSOGPLM.mjs +15 -0
  21. package/dist/chunk-7P6ASYW6.mjs +9 -0
  22. package/dist/chunk-OGSFNTKA.mjs +13 -0
  23. package/dist/client-auth.cjs +115 -0
  24. package/dist/client-auth.d.ts +43 -0
  25. package/dist/client-auth.d.ts.map +1 -0
  26. package/dist/client-auth.js +112 -0
  27. package/dist/client-auth.mjs +84 -0
  28. package/dist/discovery.cjs +86 -0
  29. package/dist/discovery.d.ts +15 -0
  30. package/dist/discovery.d.ts.map +1 -0
  31. package/dist/discovery.js +51 -0
  32. package/dist/discovery.mjs +63 -0
  33. package/dist/dpop-LVPWWADA.mjs +80 -0
  34. package/dist/dpop-NS7NKOWM.mjs +79 -0
  35. package/dist/dpop-store.cjs +152 -0
  36. package/dist/dpop-store.d.ts +37 -0
  37. package/dist/dpop-store.d.ts.map +1 -0
  38. package/dist/dpop-store.js +119 -0
  39. package/dist/dpop-store.mjs +121 -0
  40. package/dist/dpop.cjs +117 -0
  41. package/dist/dpop.d.ts +72 -0
  42. package/dist/dpop.d.ts.map +1 -0
  43. package/dist/dpop.js +114 -0
  44. package/dist/dpop.mjs +86 -0
  45. package/dist/env-config.cjs +366 -0
  46. package/dist/env-config.d.ts +103 -0
  47. package/dist/env-config.d.ts.map +1 -0
  48. package/dist/env-config.js +184 -0
  49. package/dist/env-config.mjs +343 -0
  50. package/dist/env-credentials.cjs +82 -0
  51. package/dist/env-credentials.d.ts +68 -0
  52. package/dist/env-credentials.d.ts.map +1 -0
  53. package/dist/env-credentials.js +76 -0
  54. package/dist/env-credentials.mjs +51 -0
  55. package/dist/index.cjs +940 -0
  56. package/dist/index.d.ts +104 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +101 -0
  59. package/dist/index.mjs +909 -0
  60. package/dist/pkce.cjs +87 -0
  61. package/dist/pkce.d.ts +58 -0
  62. package/dist/pkce.d.ts.map +1 -0
  63. package/dist/pkce.js +112 -0
  64. package/dist/pkce.mjs +66 -0
  65. package/dist/presets.cjs +240 -0
  66. package/dist/presets.d.ts +140 -0
  67. package/dist/presets.d.ts.map +1 -0
  68. package/dist/presets.js +182 -0
  69. package/dist/presets.mjs +217 -0
  70. package/dist/return-to.cjs +46 -0
  71. package/dist/return-to.d.ts +49 -0
  72. package/dist/return-to.d.ts.map +1 -0
  73. package/dist/return-to.js +71 -0
  74. package/dist/return-to.mjs +25 -0
  75. package/dist/roles.cjs +56 -0
  76. package/dist/roles.d.ts +35 -0
  77. package/dist/roles.d.ts.map +1 -0
  78. package/dist/roles.js +37 -0
  79. package/dist/roles.mjs +35 -0
  80. package/dist/server-client.cjs +134 -0
  81. package/dist/server-client.d.ts +12 -0
  82. package/dist/server-client.d.ts.map +1 -0
  83. package/dist/server-client.js +107 -0
  84. package/dist/server-client.mjs +111 -0
  85. package/dist/server.cjs +470 -0
  86. package/dist/server.d.ts +89 -0
  87. package/dist/server.d.ts.map +1 -0
  88. package/dist/server.js +360 -0
  89. package/dist/server.mjs +441 -0
  90. package/dist/service-client.cjs +244 -0
  91. package/dist/service-client.d.ts +89 -0
  92. package/dist/service-client.d.ts.map +1 -0
  93. package/dist/service-client.js +112 -0
  94. package/dist/service-client.mjs +213 -0
  95. package/dist/service.cjs +903 -0
  96. package/dist/service.d.ts +96 -0
  97. package/dist/service.d.ts.map +1 -0
  98. package/dist/service.js +129 -0
  99. package/dist/service.mjs +874 -0
  100. package/dist/tokens.cjs +29 -0
  101. package/dist/tokens.d.ts +13 -0
  102. package/dist/tokens.d.ts.map +1 -0
  103. package/dist/tokens.js +21 -0
  104. package/dist/tokens.mjs +8 -0
  105. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  106. package/dist/types.cjs +33 -0
  107. package/dist/types.d.ts +239 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +23 -0
  110. package/dist/types.mjs +12 -0
  111. package/dist/verify.cjs +724 -0
  112. package/dist/verify.d.ts +205 -0
  113. package/dist/verify.d.ts.map +1 -0
  114. package/dist/verify.js +371 -0
  115. package/dist/verify.mjs +590 -0
  116. package/package.json +121 -0
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/callback.ts
21
+ var callback_exports = {};
22
+ __export(callback_exports, {
23
+ OAuthCallback: () => OAuthCallback,
24
+ useOAuthCallback: () => useOAuthCallback
25
+ });
26
+ module.exports = __toCommonJS(callback_exports);
27
+ var import_core = require("@fluixi/core");
28
+ var import_signal = require("@fluixi/reactive/signal");
29
+ var import_di2 = require("@fluixi/start/di");
30
+
31
+ // src/tokens.ts
32
+ var import_di = require("@fluixi/start/di");
33
+ var AuthServiceToken = (0, import_di.injectionToken)("AuthService");
34
+ var AuthClientToken = (0, import_di.injectionToken)("AuthClient");
35
+
36
+ // src/service.ts
37
+ var import_session = require("@fluixi/session");
38
+ var import_client = require("@fluixi/auth/client");
39
+ function paramsFromUrl(url) {
40
+ const href = url ?? (typeof location === "undefined" ? null : location.href);
41
+ if (!href) return {};
42
+ const { searchParams } = new URL(String(href));
43
+ return {
44
+ code: searchParams.get("code") ?? void 0,
45
+ state: searchParams.get("state") ?? void 0,
46
+ error: searchParams.get("error") ?? void 0,
47
+ errorDescription: searchParams.get("error_description") ?? void 0
48
+ };
49
+ }
50
+
51
+ // src/callback.ts
52
+ function reasonFor(cause) {
53
+ if (cause instanceof Error) return cause.message;
54
+ return String(cause);
55
+ }
56
+ var spent = /* @__PURE__ */ new Set();
57
+ function useOAuthCallback(options = {}) {
58
+ const stage = (0, import_signal.signal)("waiting");
59
+ const error = (0, import_signal.signal)(null);
60
+ const destination = (0, import_signal.signal)(null);
61
+ const params = (0, import_signal.signal)({});
62
+ let lastQuery = null;
63
+ let injected = null;
64
+ let injectionError = null;
65
+ if (!options.service) {
66
+ try {
67
+ injected = (0, import_di2.inject)(AuthServiceToken);
68
+ } catch (cause) {
69
+ injectionError = cause;
70
+ }
71
+ }
72
+ (0, import_core.onMount)(() => {
73
+ if (typeof location !== "undefined") {
74
+ const query = new URL(location.href).search;
75
+ if (query !== lastQuery) {
76
+ lastQuery = query;
77
+ params.set(Object.fromEntries(new URL(location.href).searchParams.entries()));
78
+ }
79
+ }
80
+ if (options.enabled && !options.enabled()) return;
81
+ const service = typeof options.service === "function" ? options.service() : options.service ?? injected;
82
+ if (!service) {
83
+ stage.set("failed");
84
+ error.set(
85
+ "No auth service to finish the flow with. Provide `AuthServiceToken`, or pass `service` to useOAuthCallback." + (injectionError ? ` Injection said: ${reasonFor(injectionError)}` : "")
86
+ );
87
+ return;
88
+ }
89
+ const callback = options.params ?? paramsFromUrl();
90
+ if (callback.error) {
91
+ stage.set("failed");
92
+ error.set(`${callback.error}: ${callback.errorDescription ?? "no description given"}`);
93
+ options.onFailed?.(callback.error, callback);
94
+ return;
95
+ }
96
+ if (callback.code) {
97
+ if (spent.has(callback.code)) return;
98
+ spent.add(callback.code);
99
+ }
100
+ stage.set("exchanging");
101
+ void service.complete(callback).then((target) => {
102
+ destination.set(target);
103
+ stage.set("signed-in");
104
+ if (options.onSignedIn) options.onSignedIn(target);
105
+ else if (typeof location !== "undefined") location.replace(target);
106
+ }).catch((cause) => {
107
+ stage.set("failed");
108
+ error.set(reasonFor(cause));
109
+ options.onFailed?.(reasonFor(cause), cause);
110
+ });
111
+ });
112
+ return { stage, error, destination, params };
113
+ }
114
+ function OAuthCallback(props = {}) {
115
+ return () => {
116
+ const { stage, error } = useOAuthCallback(props);
117
+ return () => {
118
+ if (stage() === "failed") {
119
+ const reason = error() ?? "The sign in did not complete.";
120
+ return props.failed ? props.failed(reason) : reason;
121
+ }
122
+ return props.pending ? props.pending() : null;
123
+ };
124
+ };
125
+ }
@@ -0,0 +1,113 @@
1
+ import { type SignalHandle } from '@fluixi/reactive/signal';
2
+ import type { AuthService } from './service.js';
3
+ import type { CallbackParams } from './types.js';
4
+ /** Anything a fluixi renderer accepts as output. */
5
+ type ComponentChild = unknown;
6
+ /** How far the exchange got. */
7
+ export type CallbackStage = 'waiting' | 'exchanging' | 'signed-in' | 'failed';
8
+ export interface OAuthCallbackOptions {
9
+ /**
10
+ * The service to finish the flow with.
11
+ *
12
+ * Left out, it is injected from `AuthServiceToken`, which is what an app providing auth
13
+ * through DI already registers. Pass one to skip DI, or when a page chooses between
14
+ * several providers.
15
+ */
16
+ service?: AuthService<any, any, any> | (() => AuthService<any, any, any>);
17
+ /**
18
+ * The parameters to exchange. Read from `location` when absent.
19
+ *
20
+ * Only worth giving in a test, or when something has already consumed the URL.
21
+ */
22
+ params?: CallbackParams;
23
+ /**
24
+ * Where to go once signed in. Defaults to `location.replace`, which is deliberate: this
25
+ * page exists for one transition, and a document navigation arrives with the session
26
+ * already established and no router to resolve.
27
+ *
28
+ * Give a function to keep the visitor in the client router instead. It runs after mount,
29
+ * so `useNavigate` is safe to have closed over from the route that renders this.
30
+ */
31
+ onSignedIn?: (destination: string) => void;
32
+ /** Called when the exchange fails, for logging. The error is rendered either way. */
33
+ onFailed?: (reason: string, cause: unknown) => void;
34
+ /**
35
+ * Whether to exchange at all. Checked on mount, so it may read the browser.
36
+ *
37
+ * For a route that is reached in more than one configuration: in server mode the provider
38
+ * redirects to the middleware's own path and no exchange belongs here, and attempting one
39
+ * produces a state mismatch that describes nothing real. Returning false leaves the stage
40
+ * at `waiting` and sets no error, so the page can say why itself.
41
+ */
42
+ enabled?: () => boolean;
43
+ }
44
+ /** What the hook reports, all reactive. */
45
+ export interface CallbackState {
46
+ /** How far it got. `waiting` until the browser takes over. */
47
+ stage: SignalHandle<CallbackStage>;
48
+ /** Why it stopped, or null. */
49
+ error: SignalHandle<string | null>;
50
+ /** Where it is going, once known. */
51
+ destination: SignalHandle<string | null>;
52
+ /** Everything the provider put on the URL, for a page that wants to show it. */
53
+ params: SignalHandle<Record<string, string>>;
54
+ }
55
+ /**
56
+ * Finish a sign in on the redirect URI.
57
+ *
58
+ * Runs the exchange on mount and reports progress. Use this when the page needs its own
59
+ * markup or wants to show what the provider sent; `OAuthCallback` wraps it for the common
60
+ * case.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * export default function Callback() {
65
+ * const { stage, error } = useOAuthCallback();
66
+ *
67
+ * return (
68
+ * <main>
69
+ * <Show when={error} fallback={<p>Signing you in…</p>}>
70
+ * <h1>Could not sign you in</h1>
71
+ * <p>{error}</p>
72
+ * <a href="/login">Try again</a>
73
+ * </Show>
74
+ * </main>
75
+ * );
76
+ * }
77
+ * ```
78
+ */
79
+ export declare function useOAuthCallback(options?: OAuthCallbackOptions): CallbackState;
80
+ export interface OAuthCallbackProps extends OAuthCallbackOptions {
81
+ /** Shown while the exchange runs. A short line is usually right. */
82
+ pending?: () => ComponentChild;
83
+ /**
84
+ * Shown when it fails, with the reason.
85
+ *
86
+ * Worth filling in. The default states the reason and offers nothing to do about it, and a
87
+ * visitor who cannot get past this page needs a way back to the login.
88
+ */
89
+ failed?: (reason: string) => ComponentChild;
90
+ }
91
+ /**
92
+ * A whole redirect-URI route.
93
+ *
94
+ * @example
95
+ * ```tsx
96
+ * // src/routes/callback/index.tsx, the entire file
97
+ * import { OAuthCallback } from '@fluixi/oauth2/callback';
98
+ *
99
+ * export default OAuthCallback({
100
+ * pending: () => <p>Signing you in…</p>,
101
+ * failed: (reason) => (
102
+ * <>
103
+ * <h1>Could not sign you in</h1>
104
+ * <p>{reason}</p>
105
+ * <a href="/login">Try again</a>
106
+ * </>
107
+ * ),
108
+ * });
109
+ * ```
110
+ */
111
+ export declare function OAuthCallback(props?: OAuthCallbackProps): () => ComponentChild;
112
+ export {};
113
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../src/callback.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAIpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,oDAAoD;AACpD,KAAK,cAAc,GAAG,OAAO,CAAC;AAE9B,gCAAgC;AAChC,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE9E,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1E;;;;OAIG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,qFAAqF;IACrF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACpD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACzB;AAED,2CAA2C;AAC3C,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,KAAK,EAAE,YAAY,CAAC,aAAa,CAAC,CAAC;IACnC,+BAA+B;IAC/B,KAAK,EAAE,YAAY,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnC,qCAAqC;IACrC,WAAW,EAAE,YAAY,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,gFAAgF;IAChF,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC9C;AAqBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAyFlF;AAED,MAAM,WAAW,kBAAmB,SAAQ,oBAAoB;IAC9D,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,cAAc,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAE,kBAAuB,GAAG,MAAM,cAAc,CAYlF"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The redirect URI route, as something an app does not have to write.
3
+ *
4
+ * `complete()` has always been here; the route around it was not, so every application
5
+ * rewrote the same forty lines and met the same four traps. Each of these produced a page
6
+ * that rendered and then did nothing, which is the worst failure to diagnose:
7
+ *
8
+ * - Reading `location` while rendering. It does not exist on the server, so the two sides
9
+ * produce different markup, hydration finds a mismatch and keeps the server's version.
10
+ * The parameters then read as absent on a URL that has them.
11
+ * - Calling a router hook in the component body. `useNavigate` resolves the router during
12
+ * render, and a route that throws there fails to hydrate, leaving markup that responds to
13
+ * nothing. This navigates with `location.replace` instead, which needs no router at all.
14
+ * - Starting the exchange during render, so it runs once on the server and again in the
15
+ * browser.
16
+ * - Building the client from state that a page has not restored yet, which looks for a
17
+ * `state` this flow never stored and reports a mismatch.
18
+ *
19
+ * All of it happens in `onMount`, and every signal starts at a value the server can render.
20
+ *
21
+ * No JSX here: an app passes its own markup, so the package stays free of a JSX runtime and
22
+ * the page looks like the rest of the application.
23
+ */
24
+ import { onMount } from '@fluixi/core';
25
+ import { signal } from '@fluixi/reactive/signal';
26
+ import { inject } from '@fluixi/start/di';
27
+ import { AuthServiceToken } from './tokens.js';
28
+ import { paramsFromUrl } from './service.js';
29
+ /** The message for a failure, preferring the provider's own words. */
30
+ function reasonFor(cause) {
31
+ if (cause instanceof Error)
32
+ return cause.message;
33
+ return String(cause);
34
+ }
35
+ /**
36
+ * Codes this document has already spent.
37
+ *
38
+ * An authorization code is single use, and the PKCE verifier is consumed with it, so a
39
+ * second exchange of the same code fails and reports a state mismatch, which describes
40
+ * nothing that is actually wrong. Anything that runs a component body more than once would
41
+ * do that: a re-render, a remount, a development refresh.
42
+ *
43
+ * Module scope rather than a closure, because a closure is made fresh by the thing being
44
+ * guarded against.
45
+ */
46
+ const spent = new Set();
47
+ /**
48
+ * Finish a sign in on the redirect URI.
49
+ *
50
+ * Runs the exchange on mount and reports progress. Use this when the page needs its own
51
+ * markup or wants to show what the provider sent; `OAuthCallback` wraps it for the common
52
+ * case.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * export default function Callback() {
57
+ * const { stage, error } = useOAuthCallback();
58
+ *
59
+ * return (
60
+ * <main>
61
+ * <Show when={error} fallback={<p>Signing you in…</p>}>
62
+ * <h1>Could not sign you in</h1>
63
+ * <p>{error}</p>
64
+ * <a href="/login">Try again</a>
65
+ * </Show>
66
+ * </main>
67
+ * );
68
+ * }
69
+ * ```
70
+ */
71
+ export function useOAuthCallback(options = {}) {
72
+ const stage = signal('waiting');
73
+ const error = signal(null);
74
+ const destination = signal(null);
75
+ const params = signal({});
76
+ /**
77
+ * Resolved now rather than on mount, because `inject` reads the owner scope that only
78
+ * exists while a component is setting up. A server render where the app provides auth on
79
+ * the client only has nothing to resolve, and nothing there needs it: the failure is
80
+ * reported at mount, where it is visible.
81
+ */
82
+ /** The query string already reported, so a repeat pass writes nothing. */
83
+ let lastQuery = null;
84
+ let injected = null;
85
+ let injectionError = null;
86
+ if (!options.service) {
87
+ try {
88
+ injected = inject(AuthServiceToken);
89
+ }
90
+ catch (cause) {
91
+ injectionError = cause;
92
+ }
93
+ }
94
+ onMount(() => {
95
+ // Reported before anything else, because a provider that refused put its reason on the
96
+ // URL and a page showing nothing is the alternative.
97
+ //
98
+ // Compared against a plain variable rather than by reading `params()`. A read here
99
+ // subscribes whatever scope is running this to the signal, so the write that follows
100
+ // schedules that scope again, which runs this again: the work is idempotent but the loop
101
+ // is not. Writing only on a change is also what stops a fresh object, which never
102
+ // compares equal, from waking every watcher on each pass.
103
+ if (typeof location !== 'undefined') {
104
+ const query = new URL(location.href).search;
105
+ if (query !== lastQuery) {
106
+ lastQuery = query;
107
+ params.set(Object.fromEntries(new URL(location.href).searchParams.entries()));
108
+ }
109
+ }
110
+ if (options.enabled && !options.enabled())
111
+ return;
112
+ const service = typeof options.service === 'function' ? options.service() : (options.service ?? injected);
113
+ if (!service) {
114
+ stage.set('failed');
115
+ error.set('No auth service to finish the flow with. Provide `AuthServiceToken`, or pass `service` to useOAuthCallback.' +
116
+ (injectionError ? ` Injection said: ${reasonFor(injectionError)}` : ''));
117
+ return;
118
+ }
119
+ const callback = options.params ?? paramsFromUrl();
120
+ if (callback.error) {
121
+ stage.set('failed');
122
+ error.set(`${callback.error}: ${callback.errorDescription ?? 'no description given'}`);
123
+ options.onFailed?.(callback.error, callback);
124
+ return;
125
+ }
126
+ // One exchange per code. A second would spend a verifier that is already gone and
127
+ // report a state mismatch for a flow that actually worked.
128
+ if (callback.code) {
129
+ if (spent.has(callback.code))
130
+ return;
131
+ spent.add(callback.code);
132
+ }
133
+ stage.set('exchanging');
134
+ void service
135
+ .complete(callback)
136
+ .then((target) => {
137
+ destination.set(target);
138
+ stage.set('signed-in');
139
+ if (options.onSignedIn)
140
+ options.onSignedIn(target);
141
+ else if (typeof location !== 'undefined')
142
+ location.replace(target);
143
+ })
144
+ .catch((cause) => {
145
+ stage.set('failed');
146
+ error.set(reasonFor(cause));
147
+ options.onFailed?.(reasonFor(cause), cause);
148
+ });
149
+ });
150
+ return { stage, error, destination, params };
151
+ }
152
+ /**
153
+ * A whole redirect-URI route.
154
+ *
155
+ * @example
156
+ * ```tsx
157
+ * // src/routes/callback/index.tsx, the entire file
158
+ * import { OAuthCallback } from '@fluixi/oauth2/callback';
159
+ *
160
+ * export default OAuthCallback({
161
+ * pending: () => <p>Signing you in…</p>,
162
+ * failed: (reason) => (
163
+ * <>
164
+ * <h1>Could not sign you in</h1>
165
+ * <p>{reason}</p>
166
+ * <a href="/login">Try again</a>
167
+ * </>
168
+ * ),
169
+ * });
170
+ * ```
171
+ */
172
+ export function OAuthCallback(props = {}) {
173
+ return () => {
174
+ const { stage, error } = useOAuthCallback(props);
175
+ return () => {
176
+ if (stage() === 'failed') {
177
+ const reason = error() ?? 'The sign in did not complete.';
178
+ return props.failed ? props.failed(reason) : reason;
179
+ }
180
+ return props.pending ? props.pending() : null;
181
+ };
182
+ };
183
+ }
@@ -0,0 +1,106 @@
1
+ import "./chunk-7P6ASYW6.mjs";
2
+
3
+ // src/callback.ts
4
+ import { onMount } from "@fluixi/core";
5
+ import { signal } from "@fluixi/reactive/signal";
6
+ import { inject } from "@fluixi/start/di";
7
+
8
+ // src/tokens.ts
9
+ import { injectionToken } from "@fluixi/start/di";
10
+ var AuthServiceToken = injectionToken("AuthService");
11
+ var AuthClientToken = injectionToken("AuthClient");
12
+
13
+ // src/service.ts
14
+ import { createSession } from "@fluixi/session";
15
+ import { createAuthHooks } from "@fluixi/auth/client";
16
+ function paramsFromUrl(url) {
17
+ const href = url ?? (typeof location === "undefined" ? null : location.href);
18
+ if (!href) return {};
19
+ const { searchParams } = new URL(String(href));
20
+ return {
21
+ code: searchParams.get("code") ?? void 0,
22
+ state: searchParams.get("state") ?? void 0,
23
+ error: searchParams.get("error") ?? void 0,
24
+ errorDescription: searchParams.get("error_description") ?? void 0
25
+ };
26
+ }
27
+
28
+ // src/callback.ts
29
+ function reasonFor(cause) {
30
+ if (cause instanceof Error) return cause.message;
31
+ return String(cause);
32
+ }
33
+ var spent = /* @__PURE__ */ new Set();
34
+ function useOAuthCallback(options = {}) {
35
+ const stage = signal("waiting");
36
+ const error = signal(null);
37
+ const destination = signal(null);
38
+ const params = signal({});
39
+ let lastQuery = null;
40
+ let injected = null;
41
+ let injectionError = null;
42
+ if (!options.service) {
43
+ try {
44
+ injected = inject(AuthServiceToken);
45
+ } catch (cause) {
46
+ injectionError = cause;
47
+ }
48
+ }
49
+ onMount(() => {
50
+ if (typeof location !== "undefined") {
51
+ const query = new URL(location.href).search;
52
+ if (query !== lastQuery) {
53
+ lastQuery = query;
54
+ params.set(Object.fromEntries(new URL(location.href).searchParams.entries()));
55
+ }
56
+ }
57
+ if (options.enabled && !options.enabled()) return;
58
+ const service = typeof options.service === "function" ? options.service() : options.service ?? injected;
59
+ if (!service) {
60
+ stage.set("failed");
61
+ error.set(
62
+ "No auth service to finish the flow with. Provide `AuthServiceToken`, or pass `service` to useOAuthCallback." + (injectionError ? ` Injection said: ${reasonFor(injectionError)}` : "")
63
+ );
64
+ return;
65
+ }
66
+ const callback = options.params ?? paramsFromUrl();
67
+ if (callback.error) {
68
+ stage.set("failed");
69
+ error.set(`${callback.error}: ${callback.errorDescription ?? "no description given"}`);
70
+ options.onFailed?.(callback.error, callback);
71
+ return;
72
+ }
73
+ if (callback.code) {
74
+ if (spent.has(callback.code)) return;
75
+ spent.add(callback.code);
76
+ }
77
+ stage.set("exchanging");
78
+ void service.complete(callback).then((target) => {
79
+ destination.set(target);
80
+ stage.set("signed-in");
81
+ if (options.onSignedIn) options.onSignedIn(target);
82
+ else if (typeof location !== "undefined") location.replace(target);
83
+ }).catch((cause) => {
84
+ stage.set("failed");
85
+ error.set(reasonFor(cause));
86
+ options.onFailed?.(reasonFor(cause), cause);
87
+ });
88
+ });
89
+ return { stage, error, destination, params };
90
+ }
91
+ function OAuthCallback(props = {}) {
92
+ return () => {
93
+ const { stage, error } = useOAuthCallback(props);
94
+ return () => {
95
+ if (stage() === "failed") {
96
+ const reason = error() ?? "The sign in did not complete.";
97
+ return props.failed ? props.failed(reason) : reason;
98
+ }
99
+ return props.pending ? props.pending() : null;
100
+ };
101
+ };
102
+ }
103
+ export {
104
+ OAuthCallback,
105
+ useOAuthCallback
106
+ };
@@ -0,0 +1,10 @@
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.1 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+
8
+ export {
9
+ __export
10
+ };
@@ -0,0 +1,15 @@
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.1 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+
3
+ // src/types.ts
4
+ var OAuthError = class extends Error {
5
+ constructor(message, code, description) {
6
+ super(message);
7
+ this.code = code;
8
+ this.description = description;
9
+ this.name = "OAuthError";
10
+ }
11
+ };
12
+
13
+ export {
14
+ OAuthError
15
+ };
@@ -0,0 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };
@@ -0,0 +1,13 @@
1
+ // src/types.ts
2
+ var OAuthError = class extends Error {
3
+ constructor(message, code, description) {
4
+ super(message);
5
+ this.code = code;
6
+ this.description = description;
7
+ this.name = "OAuthError";
8
+ }
9
+ };
10
+
11
+ export {
12
+ OAuthError
13
+ };