@fluixi/oauth2 1.0.0-alpha.9 → 1.0.0-alpha.99
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 +1 -1
- package/dist/backchannel.cjs +462 -0
- package/dist/backchannel.d.ts +39 -0
- package/dist/backchannel.d.ts.map +1 -0
- package/dist/backchannel.js +97 -0
- package/dist/backchannel.mjs +307 -0
- package/dist/{browser-PKG6L5U5.mjs → browser-LQSO5OS2.mjs} +2 -2
- package/dist/browser.cjs +6 -1
- package/dist/browser.mjs +6 -1
- package/dist/callback.cjs +2 -2
- package/dist/callback.js +1 -1
- package/dist/callback.mjs +3 -3
- package/dist/chunk-32562UUL.mjs +108 -0
- package/dist/{chunk-R7EX3WEX.mjs → chunk-P6SSCHQX.mjs} +1 -1
- package/dist/{chunk-NROGDLGR.mjs → chunk-V27WQGTT.mjs} +1 -1
- package/dist/discovery.cjs +6 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +6 -1
- package/dist/discovery.mjs +6 -1
- package/dist/dpop-2Q6R3KLB.mjs +22 -0
- package/dist/{dpop-Y4AOLAWB.mjs → dpop-4FGDUD4B.mjs} +4 -4
- package/dist/index.cjs +108 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.mjs +112 -12
- package/dist/module.cjs +1 -1
- package/dist/module.mjs +1 -1
- package/dist/server.cjs +649 -137
- package/dist/server.d.ts +64 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +376 -61
- package/dist/server.mjs +532 -139
- package/dist/service-client.cjs +7 -2
- package/dist/service-client.mjs +9 -4
- package/dist/service.cjs +104 -5
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +38 -6
- package/dist/service.mjs +105 -6
- package/dist/session-monitor.cjs +98 -0
- package/dist/session-monitor.d.ts +32 -0
- package/dist/session-monitor.d.ts.map +1 -0
- package/dist/session-monitor.js +104 -0
- package/dist/session-monitor.mjs +77 -0
- package/dist/tokens.cjs +1 -1
- package/dist/tokens.d.ts +2 -2
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +1 -1
- package/dist/tokens.mjs +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.cjs +7 -2
- package/dist/verify.mjs +12 -7
- package/package.json +17 -15
package/dist/service-client.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.99 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -126,8 +126,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
126
126
|
try {
|
|
127
127
|
response = await fetcher(documentUrl(issuer));
|
|
128
128
|
} catch (cause) {
|
|
129
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
129
130
|
throw new OAuthError(
|
|
130
|
-
|
|
131
|
+
typeof window === "undefined" ? (
|
|
132
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
133
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
134
|
+
) : `Could not read the discovery document at ${documentUrl(issuer)}. In a browser this is usually the provider refusing a cross-origin read: give \`endpoints\` directly, or resolve them on a server. (${why})`,
|
|
131
135
|
"discovery_unreachable"
|
|
132
136
|
);
|
|
133
137
|
}
|
|
@@ -152,6 +156,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
152
156
|
jwks: doc.jwks_uri,
|
|
153
157
|
introspection: doc.introspection_endpoint,
|
|
154
158
|
revocation: doc.revocation_endpoint,
|
|
159
|
+
checkSession: doc.check_session_iframe,
|
|
155
160
|
issuer: doc.issuer ?? issuer
|
|
156
161
|
};
|
|
157
162
|
})();
|
package/dist/service-client.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
2
|
-
import "./chunk-
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.99 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
import "./chunk-P6SSCHQX.mjs";
|
|
3
3
|
|
|
4
4
|
// src/types.ts
|
|
5
5
|
var OAuthError = class extends Error {
|
|
@@ -42,7 +42,7 @@ function assertUsable(auth, who) {
|
|
|
42
42
|
}
|
|
43
43
|
async function loadJose() {
|
|
44
44
|
try {
|
|
45
|
-
return await import("./browser-
|
|
45
|
+
return await import("./browser-LQSO5OS2.mjs");
|
|
46
46
|
} catch {
|
|
47
47
|
throw new OAuthError(
|
|
48
48
|
"private_key_jwt needs `jose`. Install it: npm i jose. It is an optional peer, so a client using a secret does not carry it.",
|
|
@@ -92,8 +92,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
92
92
|
try {
|
|
93
93
|
response = await fetcher(documentUrl(issuer));
|
|
94
94
|
} catch (cause) {
|
|
95
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
95
96
|
throw new OAuthError(
|
|
96
|
-
|
|
97
|
+
typeof window === "undefined" ? (
|
|
98
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
99
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
100
|
+
) : `Could not read the discovery document at ${documentUrl(issuer)}. In a browser this is usually the provider refusing a cross-origin read: give \`endpoints\` directly, or resolve them on a server. (${why})`,
|
|
97
101
|
"discovery_unreachable"
|
|
98
102
|
);
|
|
99
103
|
}
|
|
@@ -118,6 +122,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
118
122
|
jwks: doc.jwks_uri,
|
|
119
123
|
introspection: doc.introspection_endpoint,
|
|
120
124
|
revocation: doc.revocation_endpoint,
|
|
125
|
+
checkSession: doc.check_session_iframe,
|
|
121
126
|
issuer: doc.issuer ?? issuer
|
|
122
127
|
};
|
|
123
128
|
})();
|
package/dist/service.cjs
CHANGED
|
@@ -334,8 +334,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
334
334
|
try {
|
|
335
335
|
response = await fetcher(documentUrl(issuer));
|
|
336
336
|
} catch (cause) {
|
|
337
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
337
338
|
throw new OAuthError(
|
|
338
|
-
|
|
339
|
+
typeof window === "undefined" ? (
|
|
340
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
341
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
342
|
+
) : `Could not read the discovery document at ${documentUrl(issuer)}. In a browser this is usually the provider refusing a cross-origin read: give \`endpoints\` directly, or resolve them on a server. (${why})`,
|
|
339
343
|
"discovery_unreachable"
|
|
340
344
|
);
|
|
341
345
|
}
|
|
@@ -360,6 +364,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
360
364
|
jwks: doc.jwks_uri,
|
|
361
365
|
introspection: doc.introspection_endpoint,
|
|
362
366
|
revocation: doc.revocation_endpoint,
|
|
367
|
+
checkSession: doc.check_session_iframe,
|
|
363
368
|
issuer: doc.issuer ?? issuer
|
|
364
369
|
};
|
|
365
370
|
})();
|
|
@@ -815,6 +820,81 @@ function createServerClient(config, fetcher = fetch) {
|
|
|
815
820
|
};
|
|
816
821
|
}
|
|
817
822
|
|
|
823
|
+
// src/session-monitor.ts
|
|
824
|
+
var SETTLE_MS = 15e3;
|
|
825
|
+
var GUARD_KEY = "fx-session-check";
|
|
826
|
+
function recentlyActed() {
|
|
827
|
+
try {
|
|
828
|
+
return Date.now() - Number(sessionStorage.getItem(GUARD_KEY) ?? 0) < SETTLE_MS;
|
|
829
|
+
} catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function markActed() {
|
|
834
|
+
try {
|
|
835
|
+
sessionStorage.setItem(GUARD_KEY, String(Date.now()));
|
|
836
|
+
} catch {
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function watchProviderSession(options = {}) {
|
|
840
|
+
if (typeof window === "undefined" || typeof document === "undefined") return () => {
|
|
841
|
+
};
|
|
842
|
+
const base = (options.basePath ?? "/auth").replace(/\/$/, "");
|
|
843
|
+
const fetcher = options.fetcher ?? fetch;
|
|
844
|
+
const onChange = options.onChange ?? (() => {
|
|
845
|
+
const login = new URLSearchParams({ prompt: "none", returnTo: location.pathname + location.search });
|
|
846
|
+
location.assign(`${base}/login?${login}`);
|
|
847
|
+
});
|
|
848
|
+
let stopped = false;
|
|
849
|
+
const cleanups = [];
|
|
850
|
+
const stop = () => {
|
|
851
|
+
stopped = true;
|
|
852
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
853
|
+
};
|
|
854
|
+
void (async () => {
|
|
855
|
+
const check = await fetcher(`${base}/session-check`, { credentials: "same-origin" }).then((response) => response.ok ? response.json() : null).catch(() => null);
|
|
856
|
+
if (stopped || !check?.frame || !check.state) return;
|
|
857
|
+
const origin = new URL(check.frame).origin;
|
|
858
|
+
const frame = document.createElement("iframe");
|
|
859
|
+
frame.src = check.frame;
|
|
860
|
+
frame.title = "session check";
|
|
861
|
+
frame.setAttribute("aria-hidden", "true");
|
|
862
|
+
frame.tabIndex = -1;
|
|
863
|
+
frame.style.cssText = "position:absolute;width:0;height:0;border:0;visibility:hidden";
|
|
864
|
+
const message = `${check.clientId} ${check.state}`;
|
|
865
|
+
const ask = () => {
|
|
866
|
+
if (!stopped && document.visibilityState === "visible") frame.contentWindow?.postMessage(message, origin);
|
|
867
|
+
};
|
|
868
|
+
let confirmed = false;
|
|
869
|
+
const answer = (event) => {
|
|
870
|
+
if (event.origin !== origin || event.source !== frame.contentWindow) return;
|
|
871
|
+
if (event.data === "unchanged") confirmed = true;
|
|
872
|
+
else if (event.data === "changed") {
|
|
873
|
+
stop();
|
|
874
|
+
if (!confirmed && recentlyActed()) return;
|
|
875
|
+
markActed();
|
|
876
|
+
onChange();
|
|
877
|
+
} else if (event.data === "error") {
|
|
878
|
+
stop();
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
window.addEventListener("message", answer);
|
|
882
|
+
frame.addEventListener("load", ask);
|
|
883
|
+
document.addEventListener("visibilitychange", ask);
|
|
884
|
+
window.addEventListener("focus", ask);
|
|
885
|
+
const timer = setInterval(ask, options.intervalMs ?? 3e3);
|
|
886
|
+
document.body.appendChild(frame);
|
|
887
|
+
cleanups.push(() => {
|
|
888
|
+
window.removeEventListener("message", answer);
|
|
889
|
+
document.removeEventListener("visibilitychange", ask);
|
|
890
|
+
window.removeEventListener("focus", ask);
|
|
891
|
+
clearInterval(timer);
|
|
892
|
+
frame.remove();
|
|
893
|
+
});
|
|
894
|
+
})();
|
|
895
|
+
return stop;
|
|
896
|
+
}
|
|
897
|
+
|
|
818
898
|
// src/service.ts
|
|
819
899
|
function paramsFromUrl(url) {
|
|
820
900
|
const href = url ?? (typeof location === "undefined" ? null : location.href);
|
|
@@ -841,16 +921,19 @@ function createAuthService(options) {
|
|
|
841
921
|
function buildAuthService(options) {
|
|
842
922
|
const client = clientFor(options);
|
|
843
923
|
let latest = null;
|
|
924
|
+
const fetchUserFor = async (token) => {
|
|
925
|
+
if (options.fetchUser) return options.fetchUser(token);
|
|
926
|
+
return client.fetchUser(token === COOKIE_SESSION ? void 0 : token);
|
|
927
|
+
};
|
|
928
|
+
const resumesFromCookie = options.config.tokens === "server" && typeof window !== "undefined";
|
|
929
|
+
const [ready, setReady] = (0, import_signal.createSignal)(!resumesFromCookie);
|
|
844
930
|
const session = (0, import_session.createSession)({
|
|
845
931
|
/** The credential is the callback, so this is the code exchange. */
|
|
846
932
|
async login(params) {
|
|
847
933
|
latest = await client.handleCallback(params);
|
|
848
934
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
849
935
|
},
|
|
850
|
-
|
|
851
|
-
if (options.fetchUser) return options.fetchUser(token);
|
|
852
|
-
return client.fetchUser(token === COOKIE_SESSION ? void 0 : token);
|
|
853
|
-
},
|
|
936
|
+
fetchUser: fetchUserFor,
|
|
854
937
|
async refresh() {
|
|
855
938
|
latest = await client.refresh();
|
|
856
939
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
@@ -873,11 +956,27 @@ function buildAuthService(options) {
|
|
|
873
956
|
});
|
|
874
957
|
const hooks = (0, import_client.createAuthHooks)({
|
|
875
958
|
session,
|
|
959
|
+
ready,
|
|
876
960
|
navigate: options.navigate,
|
|
877
961
|
loginPath: options.loginPath ?? "/login",
|
|
878
962
|
homePath: options.homePath ?? "/",
|
|
879
963
|
forbiddenPath: options.forbiddenPath ?? "/403"
|
|
880
964
|
});
|
|
965
|
+
if (resumesFromCookie && options.config.tokens === "server" && options.config.watchSession !== false) {
|
|
966
|
+
watchProviderSession({ basePath: options.config.basePath });
|
|
967
|
+
}
|
|
968
|
+
const renderedSignedOut = hooks.store.seeded && hooks.store.value().user == null;
|
|
969
|
+
if (resumesFromCookie && renderedSignedOut) setReady(true);
|
|
970
|
+
else if (resumesFromCookie) {
|
|
971
|
+
void (async () => {
|
|
972
|
+
try {
|
|
973
|
+
await session.adopt(COOKIE_SESSION, await fetchUserFor(COOKIE_SESSION));
|
|
974
|
+
} catch {
|
|
975
|
+
} finally {
|
|
976
|
+
setReady(true);
|
|
977
|
+
}
|
|
978
|
+
})();
|
|
979
|
+
}
|
|
881
980
|
return Object.assign(session, {
|
|
882
981
|
client,
|
|
883
982
|
protectedRoute: hooks.protectedRoute,
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAO7E,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAU,MAAM,YAAY,CAAC;AAE/F,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC;AAE7C,MAAM,WAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO;IACvE,mDAAmD;IACnD,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAChD,+FAA+F;IAC/F,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,qCAAqC;IACrC,WAAW,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAE/C;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACxC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAChE,SAAQ,OAAO,CAAC,CAAC,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAChD,IAAI,CACF,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAC3C,gBAAgB,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CACxE;IACH,oFAAoF;IACpF,MAAM,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,cAAc,CAWhE;AAUD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,EACrE,OAAO,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACnC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAStB"}
|
package/dist/service.js
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* exchange and the redirect itself is a separate call that never returns. Everything
|
|
13
13
|
* else lines up directly: `fetchUser`, `refresh`, `logout` and `getExpiry`.
|
|
14
14
|
*/
|
|
15
|
-
import { markReactiveScope, openReactiveScope, withReactiveScope } from '@fluixi/reactive/signal';
|
|
15
|
+
import { createSignal, markReactiveScope, openReactiveScope, withReactiveScope } from '@fluixi/reactive/signal';
|
|
16
16
|
import { createSession } from '@fluixi/session';
|
|
17
17
|
import { createAuthHooks } from '@fluixi/auth/client';
|
|
18
18
|
import { expiryFromJwt } from './pkce.js';
|
|
19
19
|
import { safeReturnTo } from './return-to.js';
|
|
20
20
|
import { createBrowserClient } from './browser.js';
|
|
21
21
|
import { createServerClient, COOKIE_SESSION } from './server-client.js';
|
|
22
|
+
import { watchProviderSession } from './session-monitor.js';
|
|
22
23
|
import { OAuthError } from './types.js';
|
|
23
24
|
/**
|
|
24
25
|
* The parameters on the current URL, which is what a callback route has.
|
|
@@ -63,17 +64,22 @@ function buildAuthService(options) {
|
|
|
63
64
|
// Held so `getExpiry` can answer for a token the session hands back, and so a
|
|
64
65
|
// server-mode session has something to report when the token is a cookie.
|
|
65
66
|
let latest = null;
|
|
67
|
+
const fetchUserFor = async (token) => {
|
|
68
|
+
if (options.fetchUser)
|
|
69
|
+
return options.fetchUser(token);
|
|
70
|
+
return client.fetchUser(token === COOKIE_SESSION ? undefined : token);
|
|
71
|
+
};
|
|
72
|
+
// With the tokens on the server, the browser learns it is signed in by asking. Until it
|
|
73
|
+
// has, the hooks keep the user the server rendered with rather than showing nobody.
|
|
74
|
+
const resumesFromCookie = options.config.tokens === 'server' && typeof window !== 'undefined';
|
|
75
|
+
const [ready, setReady] = createSignal(!resumesFromCookie);
|
|
66
76
|
const session = createSession({
|
|
67
77
|
/** The credential is the callback, so this is the code exchange. */
|
|
68
78
|
async login(params) {
|
|
69
79
|
latest = await client.handleCallback(params);
|
|
70
80
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
71
81
|
},
|
|
72
|
-
|
|
73
|
-
if (options.fetchUser)
|
|
74
|
-
return options.fetchUser(token);
|
|
75
|
-
return client.fetchUser(token === COOKIE_SESSION ? undefined : token);
|
|
76
|
-
},
|
|
82
|
+
fetchUser: fetchUserFor,
|
|
77
83
|
async refresh() {
|
|
78
84
|
latest = await client.refresh();
|
|
79
85
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
@@ -101,11 +107,37 @@ function buildAuthService(options) {
|
|
|
101
107
|
// rather than a second file an app has to wire up and keep in step.
|
|
102
108
|
const hooks = createAuthHooks({
|
|
103
109
|
session: session,
|
|
110
|
+
ready,
|
|
104
111
|
navigate: options.navigate,
|
|
105
112
|
loginPath: options.loginPath ?? '/login',
|
|
106
113
|
homePath: options.homePath ?? '/',
|
|
107
114
|
forbiddenPath: options.forbiddenPath ?? '/403',
|
|
108
115
|
});
|
|
116
|
+
// The page the server rendered already says whether anyone is signed in. When it said
|
|
117
|
+
// nobody, there is no session to resume: asking would only get a 401 on every anonymous
|
|
118
|
+
// page. A client-only load has no such answer and still asks.
|
|
119
|
+
// Signed in or not, a session that changes at the provider while the page is open is
|
|
120
|
+
// noticed here rather than at the next page load.
|
|
121
|
+
if (resumesFromCookie && options.config.tokens === 'server' && options.config.watchSession !== false) {
|
|
122
|
+
watchProviderSession({ basePath: options.config.basePath });
|
|
123
|
+
}
|
|
124
|
+
const renderedSignedOut = hooks.store.seeded && hooks.store.value().user == null;
|
|
125
|
+
if (resumesFromCookie && renderedSignedOut)
|
|
126
|
+
setReady(true);
|
|
127
|
+
else if (resumesFromCookie) {
|
|
128
|
+
void (async () => {
|
|
129
|
+
try {
|
|
130
|
+
// The user first: a cookie the server no longer accepts adopts nothing.
|
|
131
|
+
await session.adopt(COOKIE_SESSION, await fetchUserFor(COOKIE_SESSION));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Signed out after all; the hooks drop the server's user once ready.
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
setReady(true);
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
109
141
|
return Object.assign(session, {
|
|
110
142
|
client,
|
|
111
143
|
protectedRoute: hooks.protectedRoute,
|
package/dist/service.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "./chunk-7P6ASYW6.mjs";
|
|
2
2
|
|
|
3
3
|
// src/service.ts
|
|
4
|
-
import { markReactiveScope, openReactiveScope, withReactiveScope } from "@fluixi/reactive/signal";
|
|
4
|
+
import { createSignal, markReactiveScope, openReactiveScope, withReactiveScope } from "@fluixi/reactive/signal";
|
|
5
5
|
import { createSession } from "@fluixi/session";
|
|
6
6
|
import { createAuthHooks } from "@fluixi/auth/client";
|
|
7
7
|
|
|
@@ -301,8 +301,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
301
301
|
try {
|
|
302
302
|
response = await fetcher(documentUrl(issuer));
|
|
303
303
|
} catch (cause) {
|
|
304
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
304
305
|
throw new OAuthError(
|
|
305
|
-
|
|
306
|
+
typeof window === "undefined" ? (
|
|
307
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
308
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
309
|
+
) : `Could not read the discovery document at ${documentUrl(issuer)}. In a browser this is usually the provider refusing a cross-origin read: give \`endpoints\` directly, or resolve them on a server. (${why})`,
|
|
306
310
|
"discovery_unreachable"
|
|
307
311
|
);
|
|
308
312
|
}
|
|
@@ -327,6 +331,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
327
331
|
jwks: doc.jwks_uri,
|
|
328
332
|
introspection: doc.introspection_endpoint,
|
|
329
333
|
revocation: doc.revocation_endpoint,
|
|
334
|
+
checkSession: doc.check_session_iframe,
|
|
330
335
|
issuer: doc.issuer ?? issuer
|
|
331
336
|
};
|
|
332
337
|
})();
|
|
@@ -782,6 +787,81 @@ function createServerClient(config, fetcher = fetch) {
|
|
|
782
787
|
};
|
|
783
788
|
}
|
|
784
789
|
|
|
790
|
+
// src/session-monitor.ts
|
|
791
|
+
var SETTLE_MS = 15e3;
|
|
792
|
+
var GUARD_KEY = "fx-session-check";
|
|
793
|
+
function recentlyActed() {
|
|
794
|
+
try {
|
|
795
|
+
return Date.now() - Number(sessionStorage.getItem(GUARD_KEY) ?? 0) < SETTLE_MS;
|
|
796
|
+
} catch {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function markActed() {
|
|
801
|
+
try {
|
|
802
|
+
sessionStorage.setItem(GUARD_KEY, String(Date.now()));
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function watchProviderSession(options = {}) {
|
|
807
|
+
if (typeof window === "undefined" || typeof document === "undefined") return () => {
|
|
808
|
+
};
|
|
809
|
+
const base = (options.basePath ?? "/auth").replace(/\/$/, "");
|
|
810
|
+
const fetcher = options.fetcher ?? fetch;
|
|
811
|
+
const onChange = options.onChange ?? (() => {
|
|
812
|
+
const login = new URLSearchParams({ prompt: "none", returnTo: location.pathname + location.search });
|
|
813
|
+
location.assign(`${base}/login?${login}`);
|
|
814
|
+
});
|
|
815
|
+
let stopped = false;
|
|
816
|
+
const cleanups = [];
|
|
817
|
+
const stop = () => {
|
|
818
|
+
stopped = true;
|
|
819
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
820
|
+
};
|
|
821
|
+
void (async () => {
|
|
822
|
+
const check = await fetcher(`${base}/session-check`, { credentials: "same-origin" }).then((response) => response.ok ? response.json() : null).catch(() => null);
|
|
823
|
+
if (stopped || !check?.frame || !check.state) return;
|
|
824
|
+
const origin = new URL(check.frame).origin;
|
|
825
|
+
const frame = document.createElement("iframe");
|
|
826
|
+
frame.src = check.frame;
|
|
827
|
+
frame.title = "session check";
|
|
828
|
+
frame.setAttribute("aria-hidden", "true");
|
|
829
|
+
frame.tabIndex = -1;
|
|
830
|
+
frame.style.cssText = "position:absolute;width:0;height:0;border:0;visibility:hidden";
|
|
831
|
+
const message = `${check.clientId} ${check.state}`;
|
|
832
|
+
const ask = () => {
|
|
833
|
+
if (!stopped && document.visibilityState === "visible") frame.contentWindow?.postMessage(message, origin);
|
|
834
|
+
};
|
|
835
|
+
let confirmed = false;
|
|
836
|
+
const answer = (event) => {
|
|
837
|
+
if (event.origin !== origin || event.source !== frame.contentWindow) return;
|
|
838
|
+
if (event.data === "unchanged") confirmed = true;
|
|
839
|
+
else if (event.data === "changed") {
|
|
840
|
+
stop();
|
|
841
|
+
if (!confirmed && recentlyActed()) return;
|
|
842
|
+
markActed();
|
|
843
|
+
onChange();
|
|
844
|
+
} else if (event.data === "error") {
|
|
845
|
+
stop();
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
window.addEventListener("message", answer);
|
|
849
|
+
frame.addEventListener("load", ask);
|
|
850
|
+
document.addEventListener("visibilitychange", ask);
|
|
851
|
+
window.addEventListener("focus", ask);
|
|
852
|
+
const timer = setInterval(ask, options.intervalMs ?? 3e3);
|
|
853
|
+
document.body.appendChild(frame);
|
|
854
|
+
cleanups.push(() => {
|
|
855
|
+
window.removeEventListener("message", answer);
|
|
856
|
+
document.removeEventListener("visibilitychange", ask);
|
|
857
|
+
window.removeEventListener("focus", ask);
|
|
858
|
+
clearInterval(timer);
|
|
859
|
+
frame.remove();
|
|
860
|
+
});
|
|
861
|
+
})();
|
|
862
|
+
return stop;
|
|
863
|
+
}
|
|
864
|
+
|
|
785
865
|
// src/service.ts
|
|
786
866
|
function paramsFromUrl(url) {
|
|
787
867
|
const href = url ?? (typeof location === "undefined" ? null : location.href);
|
|
@@ -808,16 +888,19 @@ function createAuthService(options) {
|
|
|
808
888
|
function buildAuthService(options) {
|
|
809
889
|
const client = clientFor(options);
|
|
810
890
|
let latest = null;
|
|
891
|
+
const fetchUserFor = async (token) => {
|
|
892
|
+
if (options.fetchUser) return options.fetchUser(token);
|
|
893
|
+
return client.fetchUser(token === COOKIE_SESSION ? void 0 : token);
|
|
894
|
+
};
|
|
895
|
+
const resumesFromCookie = options.config.tokens === "server" && typeof window !== "undefined";
|
|
896
|
+
const [ready, setReady] = createSignal(!resumesFromCookie);
|
|
811
897
|
const session = createSession({
|
|
812
898
|
/** The credential is the callback, so this is the code exchange. */
|
|
813
899
|
async login(params) {
|
|
814
900
|
latest = await client.handleCallback(params);
|
|
815
901
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
816
902
|
},
|
|
817
|
-
|
|
818
|
-
if (options.fetchUser) return options.fetchUser(token);
|
|
819
|
-
return client.fetchUser(token === COOKIE_SESSION ? void 0 : token);
|
|
820
|
-
},
|
|
903
|
+
fetchUser: fetchUserFor,
|
|
821
904
|
async refresh() {
|
|
822
905
|
latest = await client.refresh();
|
|
823
906
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
@@ -840,11 +923,27 @@ function buildAuthService(options) {
|
|
|
840
923
|
});
|
|
841
924
|
const hooks = createAuthHooks({
|
|
842
925
|
session,
|
|
926
|
+
ready,
|
|
843
927
|
navigate: options.navigate,
|
|
844
928
|
loginPath: options.loginPath ?? "/login",
|
|
845
929
|
homePath: options.homePath ?? "/",
|
|
846
930
|
forbiddenPath: options.forbiddenPath ?? "/403"
|
|
847
931
|
});
|
|
932
|
+
if (resumesFromCookie && options.config.tokens === "server" && options.config.watchSession !== false) {
|
|
933
|
+
watchProviderSession({ basePath: options.config.basePath });
|
|
934
|
+
}
|
|
935
|
+
const renderedSignedOut = hooks.store.seeded && hooks.store.value().user == null;
|
|
936
|
+
if (resumesFromCookie && renderedSignedOut) setReady(true);
|
|
937
|
+
else if (resumesFromCookie) {
|
|
938
|
+
void (async () => {
|
|
939
|
+
try {
|
|
940
|
+
await session.adopt(COOKIE_SESSION, await fetchUserFor(COOKIE_SESSION));
|
|
941
|
+
} catch {
|
|
942
|
+
} finally {
|
|
943
|
+
setReady(true);
|
|
944
|
+
}
|
|
945
|
+
})();
|
|
946
|
+
}
|
|
848
947
|
return Object.assign(session, {
|
|
849
948
|
client,
|
|
850
949
|
protectedRoute: hooks.protectedRoute,
|
|
@@ -0,0 +1,98 @@
|
|
|
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/session-monitor.ts
|
|
21
|
+
var session_monitor_exports = {};
|
|
22
|
+
__export(session_monitor_exports, {
|
|
23
|
+
watchProviderSession: () => watchProviderSession
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(session_monitor_exports);
|
|
26
|
+
var SETTLE_MS = 15e3;
|
|
27
|
+
var GUARD_KEY = "fx-session-check";
|
|
28
|
+
function recentlyActed() {
|
|
29
|
+
try {
|
|
30
|
+
return Date.now() - Number(sessionStorage.getItem(GUARD_KEY) ?? 0) < SETTLE_MS;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function markActed() {
|
|
36
|
+
try {
|
|
37
|
+
sessionStorage.setItem(GUARD_KEY, String(Date.now()));
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function watchProviderSession(options = {}) {
|
|
42
|
+
if (typeof window === "undefined" || typeof document === "undefined") return () => {
|
|
43
|
+
};
|
|
44
|
+
const base = (options.basePath ?? "/auth").replace(/\/$/, "");
|
|
45
|
+
const fetcher = options.fetcher ?? fetch;
|
|
46
|
+
const onChange = options.onChange ?? (() => {
|
|
47
|
+
const login = new URLSearchParams({ prompt: "none", returnTo: location.pathname + location.search });
|
|
48
|
+
location.assign(`${base}/login?${login}`);
|
|
49
|
+
});
|
|
50
|
+
let stopped = false;
|
|
51
|
+
const cleanups = [];
|
|
52
|
+
const stop = () => {
|
|
53
|
+
stopped = true;
|
|
54
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
55
|
+
};
|
|
56
|
+
void (async () => {
|
|
57
|
+
const check = await fetcher(`${base}/session-check`, { credentials: "same-origin" }).then((response) => response.ok ? response.json() : null).catch(() => null);
|
|
58
|
+
if (stopped || !check?.frame || !check.state) return;
|
|
59
|
+
const origin = new URL(check.frame).origin;
|
|
60
|
+
const frame = document.createElement("iframe");
|
|
61
|
+
frame.src = check.frame;
|
|
62
|
+
frame.title = "session check";
|
|
63
|
+
frame.setAttribute("aria-hidden", "true");
|
|
64
|
+
frame.tabIndex = -1;
|
|
65
|
+
frame.style.cssText = "position:absolute;width:0;height:0;border:0;visibility:hidden";
|
|
66
|
+
const message = `${check.clientId} ${check.state}`;
|
|
67
|
+
const ask = () => {
|
|
68
|
+
if (!stopped && document.visibilityState === "visible") frame.contentWindow?.postMessage(message, origin);
|
|
69
|
+
};
|
|
70
|
+
let confirmed = false;
|
|
71
|
+
const answer = (event) => {
|
|
72
|
+
if (event.origin !== origin || event.source !== frame.contentWindow) return;
|
|
73
|
+
if (event.data === "unchanged") confirmed = true;
|
|
74
|
+
else if (event.data === "changed") {
|
|
75
|
+
stop();
|
|
76
|
+
if (!confirmed && recentlyActed()) return;
|
|
77
|
+
markActed();
|
|
78
|
+
onChange();
|
|
79
|
+
} else if (event.data === "error") {
|
|
80
|
+
stop();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
window.addEventListener("message", answer);
|
|
84
|
+
frame.addEventListener("load", ask);
|
|
85
|
+
document.addEventListener("visibilitychange", ask);
|
|
86
|
+
window.addEventListener("focus", ask);
|
|
87
|
+
const timer = setInterval(ask, options.intervalMs ?? 3e3);
|
|
88
|
+
document.body.appendChild(frame);
|
|
89
|
+
cleanups.push(() => {
|
|
90
|
+
window.removeEventListener("message", answer);
|
|
91
|
+
document.removeEventListener("visibilitychange", ask);
|
|
92
|
+
window.removeEventListener("focus", ask);
|
|
93
|
+
clearInterval(timer);
|
|
94
|
+
frame.remove();
|
|
95
|
+
});
|
|
96
|
+
})();
|
|
97
|
+
return stop;
|
|
98
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenID Connect Session Management, the app's half.
|
|
3
|
+
*
|
|
4
|
+
* The provider's check page sits in a hidden iframe. The page posts it `client_id` and the
|
|
5
|
+
* `session_state` of the last authorization answer; the check page recomputes that value
|
|
6
|
+
* from the provider's own cookie and answers `changed` or `unchanged`, with no request.
|
|
7
|
+
* `changed` means someone signed in, out, or switched account at the provider since: the
|
|
8
|
+
* app asks again silently, which signs it in, out, or as the other account.
|
|
9
|
+
*
|
|
10
|
+
* Only on the same site as the provider, or where the browser lets a framed page read its
|
|
11
|
+
* cookies. Elsewhere the check page sees no cookie and answers `changed` at once, every
|
|
12
|
+
* time: the guard below tells that apart from a real change, which follows an `unchanged`.
|
|
13
|
+
*/
|
|
14
|
+
export interface SessionCheck {
|
|
15
|
+
/** The provider's check page, or null when it has none. */
|
|
16
|
+
frame: string | null;
|
|
17
|
+
clientId: string;
|
|
18
|
+
/** The last answer's `session_state`, or null when there is nothing to compare. */
|
|
19
|
+
state: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface WatchOptions {
|
|
22
|
+
/** Where the server routes are mounted. Default `/auth`. */
|
|
23
|
+
basePath?: string;
|
|
24
|
+
/** How often to ask while the page is visible. Default 3 seconds. */
|
|
25
|
+
intervalMs?: number;
|
|
26
|
+
/** What a change does. Default: a silent sign-in that returns to this page. */
|
|
27
|
+
onChange?: () => void;
|
|
28
|
+
fetcher?: typeof fetch;
|
|
29
|
+
}
|
|
30
|
+
/** Starts watching; returns a function that stops. Does nothing outside a browser. */
|
|
31
|
+
export declare function watchProviderSession(options?: WatchOptions): () => void;
|
|
32
|
+
//# sourceMappingURL=session-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-monitor.d.ts","sourceRoot":"","sources":["../src/session-monitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,YAAY;IAC3B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAyBD,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,YAAiB,GAAG,MAAM,IAAI,CAiE3E"}
|