@fluixi/oauth2 1.0.0-alpha.1 → 1.0.0-alpha.100
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-ONMHX3Z3.mjs → browser-TV25RBLT.mjs} +2 -2
- package/dist/browser.cjs +10 -3
- package/dist/browser.mjs +10 -3
- package/dist/callback.cjs +8 -7
- package/dist/callback.js +1 -1
- package/dist/callback.mjs +3 -2
- package/dist/{chunk-4NSOGPLM.mjs → chunk-2B3I72DV.mjs} +1 -1
- package/dist/chunk-KOP5Z4LB.mjs +108 -0
- package/dist/{chunk-25WE6UWI.mjs → chunk-KTGVSZE6.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-O3HWR7AH.mjs +22 -0
- package/dist/{dpop-LVPWWADA.mjs → dpop-SGWG3K2I.mjs} +26 -6
- package/dist/{dpop-NS7NKOWM.mjs → dpop-YPGCGL7S.mjs} +22 -2
- package/dist/dpop-store.cjs +4 -2
- package/dist/dpop-store.mjs +4 -2
- package/dist/dpop.cjs +22 -2
- package/dist/dpop.d.ts +18 -1
- package/dist/dpop.d.ts.map +1 -1
- package/dist/dpop.js +23 -2
- package/dist/dpop.mjs +22 -2
- package/dist/index.cjs +135 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.mjs +138 -13
- package/dist/module.cjs +44 -0
- package/dist/module.d.ts +13 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +22 -0
- package/dist/module.mjs +23 -0
- package/dist/server.cjs +755 -80
- package/dist/server.d.ts +91 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +518 -65
- package/dist/server.mjs +638 -82
- package/dist/service-client.cjs +7 -2
- package/dist/service-client.mjs +9 -4
- package/dist/service.cjs +116 -7
- package/dist/service.d.ts +2 -18
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +48 -5
- package/dist/service.mjs +116 -7
- 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 +12 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -12
- package/dist/verify.cjs +30 -4
- package/dist/verify.mjs +12 -7
- package/package.json +22 -16
package/dist/service-client.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.100 | (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.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
import "./chunk-KTGVSZE6.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-TV25RBLT.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
|
@@ -34,6 +34,7 @@ __export(service_exports, {
|
|
|
34
34
|
paramsFromUrl: () => paramsFromUrl
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(service_exports);
|
|
37
|
+
var import_signal = require("@fluixi/reactive/signal");
|
|
37
38
|
var import_session = require("@fluixi/session");
|
|
38
39
|
var import_client = require("@fluixi/auth/client");
|
|
39
40
|
|
|
@@ -191,9 +192,11 @@ async function loadJose2() {
|
|
|
191
192
|
);
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
|
-
async function createDpopKey(alg = "ES256") {
|
|
195
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
195
196
|
const jose = await loadJose2();
|
|
196
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
197
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
198
|
+
extractable: options.extractable ?? false
|
|
199
|
+
});
|
|
197
200
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
198
201
|
return {
|
|
199
202
|
privateKey,
|
|
@@ -331,8 +334,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
331
334
|
try {
|
|
332
335
|
response = await fetcher(documentUrl(issuer));
|
|
333
336
|
} catch (cause) {
|
|
337
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
334
338
|
throw new OAuthError(
|
|
335
|
-
|
|
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})`,
|
|
336
343
|
"discovery_unreachable"
|
|
337
344
|
);
|
|
338
345
|
}
|
|
@@ -357,6 +364,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
357
364
|
jwks: doc.jwks_uri,
|
|
358
365
|
introspection: doc.introspection_endpoint,
|
|
359
366
|
revocation: doc.revocation_endpoint,
|
|
367
|
+
checkSession: doc.check_session_iframe,
|
|
360
368
|
issuer: doc.issuer ?? issuer
|
|
361
369
|
};
|
|
362
370
|
})();
|
|
@@ -812,6 +820,81 @@ function createServerClient(config, fetcher = fetch) {
|
|
|
812
820
|
};
|
|
813
821
|
}
|
|
814
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
|
+
|
|
815
898
|
// src/service.ts
|
|
816
899
|
function paramsFromUrl(url) {
|
|
817
900
|
const href = url ?? (typeof location === "undefined" ? null : location.href);
|
|
@@ -829,18 +912,28 @@ function clientFor(options) {
|
|
|
829
912
|
return options.config.tokens === "server" ? createServerClient(options.config) : createBrowserClient(options.config);
|
|
830
913
|
}
|
|
831
914
|
function createAuthService(options) {
|
|
915
|
+
const tokens = options.config.tokens;
|
|
916
|
+
const scope = (0, import_signal.openReactiveScope)({ kind: "service", name: "AuthService", ...tokens ? { detail: `tokens: ${tokens}` } : {} });
|
|
917
|
+
const service = (0, import_signal.withReactiveScope)(scope, () => buildAuthService(options));
|
|
918
|
+
(0, import_signal.markReactiveScope)(service, scope);
|
|
919
|
+
return service;
|
|
920
|
+
}
|
|
921
|
+
function buildAuthService(options) {
|
|
832
922
|
const client = clientFor(options);
|
|
833
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);
|
|
834
930
|
const session = (0, import_session.createSession)({
|
|
835
931
|
/** The credential is the callback, so this is the code exchange. */
|
|
836
932
|
async login(params) {
|
|
837
933
|
latest = await client.handleCallback(params);
|
|
838
934
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
839
935
|
},
|
|
840
|
-
|
|
841
|
-
if (options.fetchUser) return options.fetchUser(token);
|
|
842
|
-
return client.fetchUser(token === COOKIE_SESSION ? void 0 : token);
|
|
843
|
-
},
|
|
936
|
+
fetchUser: fetchUserFor,
|
|
844
937
|
async refresh() {
|
|
845
938
|
latest = await client.refresh();
|
|
846
939
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
@@ -863,11 +956,27 @@ function createAuthService(options) {
|
|
|
863
956
|
});
|
|
864
957
|
const hooks = (0, import_client.createAuthHooks)({
|
|
865
958
|
session,
|
|
959
|
+
ready,
|
|
866
960
|
navigate: options.navigate,
|
|
867
961
|
loginPath: options.loginPath ?? "/login",
|
|
868
962
|
homePath: options.homePath ?? "/",
|
|
869
963
|
forbiddenPath: options.forbiddenPath ?? "/403"
|
|
870
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
|
+
}
|
|
871
980
|
return Object.assign(session, {
|
|
872
981
|
client,
|
|
873
982
|
protectedRoute: hooks.protectedRoute,
|
package/dist/service.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The application-facing half.
|
|
3
|
-
*
|
|
4
|
-
* `AuthClient` knows the protocol. This knows the application: who is signed in, whether
|
|
5
|
-
* the session is still good, when to refresh, and where to send someone who is not
|
|
6
|
-
* allowed in. It is built on `@fluixi/session`, which already owns that state machine,
|
|
7
|
-
* rather than a second one written here.
|
|
8
|
-
*
|
|
9
|
-
* The mapping onto a session adapter is the part worth explaining. `createSession`'s
|
|
10
|
-
* `login(creds)` means "turn these credentials into a token". In a redirect flow the
|
|
11
|
-
* credential is the authorization code on the callback URL, so `login` is the code
|
|
12
|
-
* exchange and the redirect itself is a separate call that never returns. Everything
|
|
13
|
-
* else lines up directly: `fetchUser`, `refresh`, `logout` and `getExpiry`.
|
|
14
|
-
*/
|
|
15
1
|
import { type AccessControl, type Session } from '@fluixi/session';
|
|
16
|
-
import { createAuthHooks } from '@fluixi/auth/client';
|
|
2
|
+
import { createAuthHooks, type AuthHooksOptions } from '@fluixi/auth/client';
|
|
17
3
|
import type { AuthClient, AuthConfig, CallbackParams, LoginOptions } from './types.js';
|
|
18
4
|
/** The credential a redirect flow produces. */
|
|
19
5
|
export type AuthCredentials = CallbackParams;
|
|
@@ -55,9 +41,7 @@ export interface AuthServiceOptions<U, P = unknown, Q = (p: P) => boolean> {
|
|
|
55
41
|
* client-side navigation; leave it out and it assigns `location`, which reloads the
|
|
56
42
|
* document and discards the reactive tree.
|
|
57
43
|
*/
|
|
58
|
-
navigate?:
|
|
59
|
-
handler: () => (to: string, options?: unknown) => void;
|
|
60
|
-
};
|
|
44
|
+
navigate?: AuthHooksOptions['navigate'];
|
|
61
45
|
/** Where a signed-out visitor is sent. Default `/login`. */
|
|
62
46
|
loginPath?: string;
|
|
63
47
|
/** Where a signed-in visitor is sent from a guest-only page. Default `/`. */
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
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,12 +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 { createSignal, markReactiveScope, openReactiveScope, withReactiveScope } from '@fluixi/reactive/signal';
|
|
15
16
|
import { createSession } from '@fluixi/session';
|
|
16
17
|
import { createAuthHooks } from '@fluixi/auth/client';
|
|
17
18
|
import { expiryFromJwt } from './pkce.js';
|
|
18
19
|
import { safeReturnTo } from './return-to.js';
|
|
19
20
|
import { createBrowserClient } from './browser.js';
|
|
20
21
|
import { createServerClient, COOKIE_SESSION } from './server-client.js';
|
|
22
|
+
import { watchProviderSession } from './session-monitor.js';
|
|
21
23
|
import { OAuthError } from './types.js';
|
|
22
24
|
/**
|
|
23
25
|
* The parameters on the current URL, which is what a callback route has.
|
|
@@ -48,21 +50,36 @@ function clientFor(options) {
|
|
|
48
50
|
: createBrowserClient(options.config);
|
|
49
51
|
}
|
|
50
52
|
export function createAuthService(options) {
|
|
53
|
+
// One group in the devtools graph for everything the service holds: the session's `user`,
|
|
54
|
+
// `token`, `status` and the rest. Marked on the instance, so `authModule(auth)` places the
|
|
55
|
+
// group under the module that provides it.
|
|
56
|
+
const tokens = options.config.tokens;
|
|
57
|
+
const scope = openReactiveScope({ kind: 'service', name: 'AuthService', ...(tokens ? { detail: `tokens: ${tokens}` } : {}) });
|
|
58
|
+
const service = withReactiveScope(scope, () => buildAuthService(options));
|
|
59
|
+
markReactiveScope(service, scope);
|
|
60
|
+
return service;
|
|
61
|
+
}
|
|
62
|
+
function buildAuthService(options) {
|
|
51
63
|
const client = clientFor(options);
|
|
52
64
|
// Held so `getExpiry` can answer for a token the session hands back, and so a
|
|
53
65
|
// server-mode session has something to report when the token is a cookie.
|
|
54
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);
|
|
55
76
|
const session = createSession({
|
|
56
77
|
/** The credential is the callback, so this is the code exchange. */
|
|
57
78
|
async login(params) {
|
|
58
79
|
latest = await client.handleCallback(params);
|
|
59
80
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
60
81
|
},
|
|
61
|
-
|
|
62
|
-
if (options.fetchUser)
|
|
63
|
-
return options.fetchUser(token);
|
|
64
|
-
return client.fetchUser(token === COOKIE_SESSION ? undefined : token);
|
|
65
|
-
},
|
|
82
|
+
fetchUser: fetchUserFor,
|
|
66
83
|
async refresh() {
|
|
67
84
|
latest = await client.refresh();
|
|
68
85
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
@@ -90,11 +107,37 @@ export function createAuthService(options) {
|
|
|
90
107
|
// rather than a second file an app has to wire up and keep in step.
|
|
91
108
|
const hooks = createAuthHooks({
|
|
92
109
|
session: session,
|
|
110
|
+
ready,
|
|
93
111
|
navigate: options.navigate,
|
|
94
112
|
loginPath: options.loginPath ?? '/login',
|
|
95
113
|
homePath: options.homePath ?? '/',
|
|
96
114
|
forbiddenPath: options.forbiddenPath ?? '/403',
|
|
97
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
|
+
}
|
|
98
141
|
return Object.assign(session, {
|
|
99
142
|
client,
|
|
100
143
|
protectedRoute: hooks.protectedRoute,
|
package/dist/service.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "./chunk-7P6ASYW6.mjs";
|
|
2
2
|
|
|
3
3
|
// src/service.ts
|
|
4
|
+
import { createSignal, markReactiveScope, openReactiveScope, withReactiveScope } from "@fluixi/reactive/signal";
|
|
4
5
|
import { createSession } from "@fluixi/session";
|
|
5
6
|
import { createAuthHooks } from "@fluixi/auth/client";
|
|
6
7
|
|
|
@@ -158,9 +159,11 @@ async function loadJose2() {
|
|
|
158
159
|
);
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
|
-
async function createDpopKey(alg = "ES256") {
|
|
162
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
162
163
|
const jose = await loadJose2();
|
|
163
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
164
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
165
|
+
extractable: options.extractable ?? false
|
|
166
|
+
});
|
|
164
167
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
165
168
|
return {
|
|
166
169
|
privateKey,
|
|
@@ -298,8 +301,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
298
301
|
try {
|
|
299
302
|
response = await fetcher(documentUrl(issuer));
|
|
300
303
|
} catch (cause) {
|
|
304
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
301
305
|
throw new OAuthError(
|
|
302
|
-
|
|
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})`,
|
|
303
310
|
"discovery_unreachable"
|
|
304
311
|
);
|
|
305
312
|
}
|
|
@@ -324,6 +331,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
324
331
|
jwks: doc.jwks_uri,
|
|
325
332
|
introspection: doc.introspection_endpoint,
|
|
326
333
|
revocation: doc.revocation_endpoint,
|
|
334
|
+
checkSession: doc.check_session_iframe,
|
|
327
335
|
issuer: doc.issuer ?? issuer
|
|
328
336
|
};
|
|
329
337
|
})();
|
|
@@ -779,6 +787,81 @@ function createServerClient(config, fetcher = fetch) {
|
|
|
779
787
|
};
|
|
780
788
|
}
|
|
781
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
|
+
|
|
782
865
|
// src/service.ts
|
|
783
866
|
function paramsFromUrl(url) {
|
|
784
867
|
const href = url ?? (typeof location === "undefined" ? null : location.href);
|
|
@@ -796,18 +879,28 @@ function clientFor(options) {
|
|
|
796
879
|
return options.config.tokens === "server" ? createServerClient(options.config) : createBrowserClient(options.config);
|
|
797
880
|
}
|
|
798
881
|
function createAuthService(options) {
|
|
882
|
+
const tokens = options.config.tokens;
|
|
883
|
+
const scope = openReactiveScope({ kind: "service", name: "AuthService", ...tokens ? { detail: `tokens: ${tokens}` } : {} });
|
|
884
|
+
const service = withReactiveScope(scope, () => buildAuthService(options));
|
|
885
|
+
markReactiveScope(service, scope);
|
|
886
|
+
return service;
|
|
887
|
+
}
|
|
888
|
+
function buildAuthService(options) {
|
|
799
889
|
const client = clientFor(options);
|
|
800
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);
|
|
801
897
|
const session = createSession({
|
|
802
898
|
/** The credential is the callback, so this is the code exchange. */
|
|
803
899
|
async login(params) {
|
|
804
900
|
latest = await client.handleCallback(params);
|
|
805
901
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
806
902
|
},
|
|
807
|
-
|
|
808
|
-
if (options.fetchUser) return options.fetchUser(token);
|
|
809
|
-
return client.fetchUser(token === COOKIE_SESSION ? void 0 : token);
|
|
810
|
-
},
|
|
903
|
+
fetchUser: fetchUserFor,
|
|
811
904
|
async refresh() {
|
|
812
905
|
latest = await client.refresh();
|
|
813
906
|
return { token: latest.accessToken, expiresAt: latest.expiresAt };
|
|
@@ -830,11 +923,27 @@ function createAuthService(options) {
|
|
|
830
923
|
});
|
|
831
924
|
const hooks = createAuthHooks({
|
|
832
925
|
session,
|
|
926
|
+
ready,
|
|
833
927
|
navigate: options.navigate,
|
|
834
928
|
loginPath: options.loginPath ?? "/login",
|
|
835
929
|
homePath: options.homePath ?? "/",
|
|
836
930
|
forbiddenPath: options.forbiddenPath ?? "/403"
|
|
837
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
|
+
}
|
|
838
947
|
return Object.assign(session, {
|
|
839
948
|
client,
|
|
840
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
|
+
}
|