@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.
- package/LICENSE +21 -0
- package/dist/api-guard.cjs +185 -0
- package/dist/api-guard.d.ts +94 -0
- package/dist/api-guard.d.ts.map +1 -0
- package/dist/api-guard.js +127 -0
- package/dist/api-guard.mjs +141 -0
- package/dist/browser-FDUOEUM2.mjs +3544 -0
- package/dist/browser-ONMHX3Z3.mjs +3545 -0
- package/dist/browser.cjs +735 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +448 -0
- package/dist/browser.mjs +704 -0
- package/dist/callback.cjs +125 -0
- package/dist/callback.d.ts +113 -0
- package/dist/callback.d.ts.map +1 -0
- package/dist/callback.js +183 -0
- package/dist/callback.mjs +106 -0
- package/dist/chunk-25WE6UWI.mjs +10 -0
- package/dist/chunk-4NSOGPLM.mjs +15 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-OGSFNTKA.mjs +13 -0
- package/dist/client-auth.cjs +115 -0
- package/dist/client-auth.d.ts +43 -0
- package/dist/client-auth.d.ts.map +1 -0
- package/dist/client-auth.js +112 -0
- package/dist/client-auth.mjs +84 -0
- package/dist/discovery.cjs +86 -0
- package/dist/discovery.d.ts +15 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +51 -0
- package/dist/discovery.mjs +63 -0
- package/dist/dpop-LVPWWADA.mjs +80 -0
- package/dist/dpop-NS7NKOWM.mjs +79 -0
- package/dist/dpop-store.cjs +152 -0
- package/dist/dpop-store.d.ts +37 -0
- package/dist/dpop-store.d.ts.map +1 -0
- package/dist/dpop-store.js +119 -0
- package/dist/dpop-store.mjs +121 -0
- package/dist/dpop.cjs +117 -0
- package/dist/dpop.d.ts +72 -0
- package/dist/dpop.d.ts.map +1 -0
- package/dist/dpop.js +114 -0
- package/dist/dpop.mjs +86 -0
- package/dist/env-config.cjs +366 -0
- package/dist/env-config.d.ts +103 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +184 -0
- package/dist/env-config.mjs +343 -0
- package/dist/env-credentials.cjs +82 -0
- package/dist/env-credentials.d.ts +68 -0
- package/dist/env-credentials.d.ts.map +1 -0
- package/dist/env-credentials.js +76 -0
- package/dist/env-credentials.mjs +51 -0
- package/dist/index.cjs +940 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.mjs +909 -0
- package/dist/pkce.cjs +87 -0
- package/dist/pkce.d.ts +58 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +112 -0
- package/dist/pkce.mjs +66 -0
- package/dist/presets.cjs +240 -0
- package/dist/presets.d.ts +140 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +182 -0
- package/dist/presets.mjs +217 -0
- package/dist/return-to.cjs +46 -0
- package/dist/return-to.d.ts +49 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +71 -0
- package/dist/return-to.mjs +25 -0
- package/dist/roles.cjs +56 -0
- package/dist/roles.d.ts +35 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +37 -0
- package/dist/roles.mjs +35 -0
- package/dist/server-client.cjs +134 -0
- package/dist/server-client.d.ts +12 -0
- package/dist/server-client.d.ts.map +1 -0
- package/dist/server-client.js +107 -0
- package/dist/server-client.mjs +111 -0
- package/dist/server.cjs +470 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +360 -0
- package/dist/server.mjs +441 -0
- package/dist/service-client.cjs +244 -0
- package/dist/service-client.d.ts +89 -0
- package/dist/service-client.d.ts.map +1 -0
- package/dist/service-client.js +112 -0
- package/dist/service-client.mjs +213 -0
- package/dist/service.cjs +903 -0
- package/dist/service.d.ts +96 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +129 -0
- package/dist/service.mjs +874 -0
- package/dist/tokens.cjs +29 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +21 -0
- package/dist/tokens.mjs +8 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +33 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.mjs +12 -0
- package/dist/verify.cjs +724 -0
- package/dist/verify.d.ts +205 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +371 -0
- package/dist/verify.mjs +590 -0
- 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"}
|
package/dist/callback.js
ADDED
|
@@ -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
|
+
};
|