oidc-spa 6.4.0 → 6.5.0
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/oidc/createOidc.d.ts +0 -2
- package/oidc/createOidc.js +307 -355
- package/oidc/createOidc.js.map +1 -1
- package/oidc/{createIsUserActive.js → isUserActive.js} +1 -1
- package/oidc/isUserActive.js.map +1 -0
- package/oidc/loginOrGoToAuthServer.d.ts +41 -0
- package/oidc/loginOrGoToAuthServer.js +296 -0
- package/oidc/loginOrGoToAuthServer.js.map +1 -0
- package/oidc/loginSilent.d.ts +2 -2
- package/oidc/loginSilent.js +2 -2
- package/oidc/loginSilent.js.map +1 -1
- package/oidc/oidcClientTsUserToTokens.d.ts +1 -0
- package/oidc/oidcClientTsUserToTokens.js +16 -0
- package/oidc/oidcClientTsUserToTokens.js.map +1 -1
- package/oidc/persistedAuthState.d.ts +9 -0
- package/oidc/persistedAuthState.js +28 -0
- package/oidc/persistedAuthState.js.map +1 -0
- package/package.json +26 -11
- package/src/oidc/createOidc.ts +291 -353
- package/src/oidc/loginOrGoToAuthServer.ts +267 -0
- package/src/oidc/loginSilent.ts +4 -4
- package/src/oidc/oidcClientTsUserToTokens.ts +24 -0
- package/src/oidc/persistedAuthState.ts +36 -0
- package/src/tools/ephemeralSessionStorage.ts +191 -0
- package/src/tools/haveSharedParentDomain.ts +13 -0
- package/tools/ephemeralSessionStorage.d.ts +3 -0
- package/tools/ephemeralSessionStorage.js +133 -0
- package/tools/ephemeralSessionStorage.js.map +1 -0
- package/tools/haveSharedParentDomain.d.ts +4 -0
- package/tools/haveSharedParentDomain.js +14 -0
- package/tools/haveSharedParentDomain.js.map +1 -0
- package/vendor/frontend/oidc-client-ts-and-jwt-decode.js +1 -1
- package/oidc/createIsUserActive.js.map +0 -1
- package/oidc/persistedLogoutState.d.ts +0 -9
- package/oidc/persistedLogoutState.js +0 -25
- package/oidc/persistedLogoutState.js.map +0 -1
- package/src/oidc/persistedLogoutState.ts +0 -29
- /package/oidc/{createIsUserActive.d.ts → isUserActive.d.ts} +0 -0
- /package/src/oidc/{createIsUserActive.ts → isUserActive.ts} +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { UserManager as OidcClientTsUserManager } from "../vendor/frontend/oidc-client-ts-and-jwt-decode";
|
|
2
|
+
import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
|
|
3
|
+
import { id, assert, type Equals } from "../vendor/frontend/tsafe";
|
|
4
|
+
import type { StateData } from "./StateData";
|
|
5
|
+
|
|
6
|
+
const GLOBAL_CONTEXT_KEY = "__oidc-spa.loginOrGoToAuthSever.globalContext";
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
[GLOBAL_CONTEXT_KEY]: {
|
|
11
|
+
hasLoginBeenCalled: boolean;
|
|
12
|
+
URL_real: typeof URL;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
window[GLOBAL_CONTEXT_KEY] ??= {
|
|
18
|
+
hasLoginBeenCalled: false,
|
|
19
|
+
URL_real: window.URL
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const globalContext = window[GLOBAL_CONTEXT_KEY];
|
|
23
|
+
|
|
24
|
+
type Params = Params.Login | Params.GoToAuthServer;
|
|
25
|
+
|
|
26
|
+
namespace Params {
|
|
27
|
+
type Common = {
|
|
28
|
+
redirectUrl: string;
|
|
29
|
+
extraQueryParams_local: Record<string, string> | undefined;
|
|
30
|
+
transformUrlBeforeRedirect_local: ((url: string) => string) | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type Login = Common & {
|
|
34
|
+
action: "login";
|
|
35
|
+
doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: boolean;
|
|
36
|
+
doForceReloadOnBfCache: boolean;
|
|
37
|
+
doForceInteraction: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type GoToAuthServer = Common & {
|
|
41
|
+
action: "go to auth server";
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createLoginOrGoToAuthServer(params: {
|
|
46
|
+
configId: string;
|
|
47
|
+
oidcClientTsUserManager: OidcClientTsUserManager;
|
|
48
|
+
getExtraQueryParams: (() => Record<string, string>) | undefined;
|
|
49
|
+
transformUrlBeforeRedirect: ((url: string) => string) | undefined;
|
|
50
|
+
homeAndCallbackUrl: string;
|
|
51
|
+
log: typeof console.log | undefined;
|
|
52
|
+
}) {
|
|
53
|
+
const {
|
|
54
|
+
configId,
|
|
55
|
+
oidcClientTsUserManager,
|
|
56
|
+
getExtraQueryParams,
|
|
57
|
+
transformUrlBeforeRedirect,
|
|
58
|
+
homeAndCallbackUrl,
|
|
59
|
+
log
|
|
60
|
+
} = params;
|
|
61
|
+
|
|
62
|
+
const LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN = `oidc-spa.login-redirect-initiated:${configId}`;
|
|
63
|
+
|
|
64
|
+
let lastPublicUrl: string | undefined = undefined;
|
|
65
|
+
|
|
66
|
+
async function loginOrGoToAuthServer(params: Params): Promise<never> {
|
|
67
|
+
const {
|
|
68
|
+
redirectUrl: redirectUrl_params,
|
|
69
|
+
extraQueryParams_local,
|
|
70
|
+
transformUrlBeforeRedirect_local,
|
|
71
|
+
...rest
|
|
72
|
+
} = params;
|
|
73
|
+
|
|
74
|
+
log?.("Calling loginOrGoToAuthServer", { params });
|
|
75
|
+
|
|
76
|
+
login_specific_handling: {
|
|
77
|
+
if (rest.action !== "login") {
|
|
78
|
+
break login_specific_handling;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (globalContext.hasLoginBeenCalled) {
|
|
82
|
+
log?.("login() has already been called, ignoring the call");
|
|
83
|
+
return new Promise<never>(() => {});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
globalContext.hasLoginBeenCalled = true;
|
|
87
|
+
|
|
88
|
+
bf_cache_handling: {
|
|
89
|
+
if (rest.doForceReloadOnBfCache) {
|
|
90
|
+
document.removeEventListener("visibilitychange", () => {
|
|
91
|
+
if (document.visibilityState === "visible") {
|
|
92
|
+
location.reload();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
break bf_cache_handling;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
localStorage.setItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN, "true");
|
|
99
|
+
|
|
100
|
+
const callback = () => {
|
|
101
|
+
if (document.visibilityState === "visible") {
|
|
102
|
+
document.removeEventListener("visibilitychange", callback);
|
|
103
|
+
|
|
104
|
+
log?.(
|
|
105
|
+
"We came back from the login pages and the state of the app has been restored"
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (rest.doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack) {
|
|
109
|
+
if (lastPublicUrl !== undefined) {
|
|
110
|
+
log?.(`Loading last public route: ${lastPublicUrl}`);
|
|
111
|
+
window.location.href = lastPublicUrl;
|
|
112
|
+
} else {
|
|
113
|
+
log?.("We don't know the last public route, navigating back in history");
|
|
114
|
+
window.history.back();
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
log?.("The current page doesn't require auth...");
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
localStorage.getItem(
|
|
121
|
+
LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN
|
|
122
|
+
) === null
|
|
123
|
+
) {
|
|
124
|
+
log?.("but the user is now authenticated, reloading the page");
|
|
125
|
+
location.reload();
|
|
126
|
+
} else {
|
|
127
|
+
log?.(
|
|
128
|
+
"and the user doesn't seem to be authenticated, avoiding a reload"
|
|
129
|
+
);
|
|
130
|
+
globalContext.hasLoginBeenCalled = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
log?.("Start listening to visibility change event");
|
|
137
|
+
|
|
138
|
+
document.addEventListener("visibilitychange", callback);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const redirectUrl = toFullyQualifiedUrl({
|
|
143
|
+
urlish: redirectUrl_params,
|
|
144
|
+
doAssertNoQueryParams: false
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
log?.(`redirectUrl: ${redirectUrl}`);
|
|
148
|
+
|
|
149
|
+
const transformUrl_oidcClientTs = (url: string) => {
|
|
150
|
+
(
|
|
151
|
+
[
|
|
152
|
+
[getExtraQueryParams?.(), transformUrlBeforeRedirect],
|
|
153
|
+
[extraQueryParams_local, transformUrlBeforeRedirect_local]
|
|
154
|
+
] as const
|
|
155
|
+
).forEach(([extraQueryParams, transformUrlBeforeRedirect]) => {
|
|
156
|
+
add_extra_query_params: {
|
|
157
|
+
if (extraQueryParams === undefined) {
|
|
158
|
+
break add_extra_query_params;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const url_obj = new URL(url);
|
|
162
|
+
|
|
163
|
+
for (const [name, value] of Object.entries(extraQueryParams)) {
|
|
164
|
+
url_obj.searchParams.set(name, value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
url = url_obj.href;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
apply_transform_before_redirect: {
|
|
171
|
+
if (transformUrlBeforeRedirect === undefined) {
|
|
172
|
+
break apply_transform_before_redirect;
|
|
173
|
+
}
|
|
174
|
+
url = transformUrlBeforeRedirect(url);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return url;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const redirectMethod = (() => {
|
|
182
|
+
switch (rest.action) {
|
|
183
|
+
case "login":
|
|
184
|
+
return rest.doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack
|
|
185
|
+
? "replace"
|
|
186
|
+
: "assign";
|
|
187
|
+
case "go to auth server":
|
|
188
|
+
return "assign";
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
|
|
192
|
+
log?.(`redirectMethod: ${redirectMethod}`);
|
|
193
|
+
|
|
194
|
+
const { extraQueryParams } = (() => {
|
|
195
|
+
const extraQueryParams: Record<string, string> = extraQueryParams_local ?? {};
|
|
196
|
+
|
|
197
|
+
read_query_params_added_by_transform_before_redirect: {
|
|
198
|
+
if (transformUrlBeforeRedirect_local === undefined) {
|
|
199
|
+
break read_query_params_added_by_transform_before_redirect;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let url_afterTransform;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
url_afterTransform = transformUrlBeforeRedirect_local("https://dummy.com");
|
|
206
|
+
} catch {
|
|
207
|
+
break read_query_params_added_by_transform_before_redirect;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const [name, value] of new URL(url_afterTransform).searchParams) {
|
|
211
|
+
extraQueryParams[name] = value;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { extraQueryParams };
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
await oidcClientTsUserManager.signinRedirect({
|
|
219
|
+
state: id<StateData>({
|
|
220
|
+
context: "redirect",
|
|
221
|
+
redirectUrl,
|
|
222
|
+
extraQueryParams,
|
|
223
|
+
hasBeenProcessedByCallback: false,
|
|
224
|
+
configId,
|
|
225
|
+
action: "login",
|
|
226
|
+
redirectUrl_consentRequiredCase: (() => {
|
|
227
|
+
switch (rest.action) {
|
|
228
|
+
case "login":
|
|
229
|
+
return lastPublicUrl ?? homeAndCallbackUrl;
|
|
230
|
+
case "go to auth server":
|
|
231
|
+
return redirectUrl;
|
|
232
|
+
}
|
|
233
|
+
})()
|
|
234
|
+
}),
|
|
235
|
+
redirectMethod,
|
|
236
|
+
prompt: (() => {
|
|
237
|
+
switch (rest.action) {
|
|
238
|
+
case "go to auth server":
|
|
239
|
+
return undefined;
|
|
240
|
+
case "login":
|
|
241
|
+
return rest.doForceInteraction ? "consent" : undefined;
|
|
242
|
+
}
|
|
243
|
+
assert<Equals<typeof rest, never>>;
|
|
244
|
+
})(),
|
|
245
|
+
transformUrl: transformUrl_oidcClientTs
|
|
246
|
+
});
|
|
247
|
+
return new Promise<never>(() => {});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function toCallBeforeReturningOidcLoggedIn() {
|
|
251
|
+
localStorage.removeItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function toCallBeforeReturningOidcNotLoggedIn() {
|
|
255
|
+
const realPushState = history.pushState.bind(history);
|
|
256
|
+
history.pushState = function pushState(...args) {
|
|
257
|
+
lastPublicUrl = window.location.href;
|
|
258
|
+
return realPushState(...args);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
loginOrGoToAuthServer,
|
|
264
|
+
toCallBeforeReturningOidcLoggedIn,
|
|
265
|
+
toCallBeforeReturningOidcNotLoggedIn
|
|
266
|
+
};
|
|
267
|
+
}
|
package/src/oidc/loginSilent.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function authResponseToUrl(authResponse: AuthResponse): string {
|
|
|
27
27
|
|
|
28
28
|
type ResultOfLoginSilent =
|
|
29
29
|
| {
|
|
30
|
-
outcome: "
|
|
30
|
+
outcome: "got auth response from iframe";
|
|
31
31
|
authResponse: AuthResponse;
|
|
32
32
|
}
|
|
33
33
|
| {
|
|
@@ -35,7 +35,7 @@ type ResultOfLoginSilent =
|
|
|
35
35
|
cause: "timeout" | "can't reach well-known oidc endpoint";
|
|
36
36
|
}
|
|
37
37
|
| {
|
|
38
|
-
outcome: "refresh token
|
|
38
|
+
outcome: "token refreshed using refresh token";
|
|
39
39
|
oidcClientTsUser: OidcClientTsUser;
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -99,7 +99,7 @@ export async function loginSilent(params: {
|
|
|
99
99
|
window.removeEventListener("message", listener);
|
|
100
100
|
|
|
101
101
|
dResult.resolve({
|
|
102
|
-
outcome: "
|
|
102
|
+
outcome: "got auth response from iframe",
|
|
103
103
|
authResponse
|
|
104
104
|
});
|
|
105
105
|
};
|
|
@@ -122,7 +122,7 @@ export async function loginSilent(params: {
|
|
|
122
122
|
clearTimeout(timeout);
|
|
123
123
|
|
|
124
124
|
dResult.resolve({
|
|
125
|
-
outcome: "refresh token
|
|
125
|
+
outcome: "token refreshed using refresh token",
|
|
126
126
|
oidcClientTsUser
|
|
127
127
|
});
|
|
128
128
|
},
|
|
@@ -117,3 +117,27 @@ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, u
|
|
|
117
117
|
|
|
118
118
|
return tokens;
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
export function getMsBeforeExpiration(tokens: Oidc.Tokens): number {
|
|
122
|
+
// NOTE: In general the access token is supposed to have a shorter
|
|
123
|
+
// lifespan than the refresh token but we don't want to make any
|
|
124
|
+
// assumption here.
|
|
125
|
+
const tokenExpirationTime = Math.min(
|
|
126
|
+
tokens.accessTokenExpirationTime,
|
|
127
|
+
tokens.refreshTokenExpirationTime
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const msBeforeExpiration = Math.min(
|
|
131
|
+
tokenExpirationTime - Date.now(),
|
|
132
|
+
// NOTE: We want to make sure we do not overflow the setTimeout
|
|
133
|
+
// that must be a 32 bit unsigned integer.
|
|
134
|
+
// This can happen if the tokenExpirationTime is more than 24.8 days in the future.
|
|
135
|
+
Math.pow(2, 31) - 1
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (msBeforeExpiration < 0) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return msBeforeExpiration;
|
|
143
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { assert } from "../vendor/frontend/tsafe";
|
|
2
|
+
|
|
3
|
+
function getKey(params: { configId: string }) {
|
|
4
|
+
const { configId } = params;
|
|
5
|
+
|
|
6
|
+
return `oidc-spa:auth-state:${configId}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type PersistedAuthState = "logged in" | "explicitly logged out";
|
|
10
|
+
|
|
11
|
+
export function persistAuthState(params: { configId: string; state: PersistedAuthState | undefined }) {
|
|
12
|
+
const { configId, state } = params;
|
|
13
|
+
|
|
14
|
+
const key = getKey({ configId });
|
|
15
|
+
|
|
16
|
+
if (state === undefined) {
|
|
17
|
+
localStorage.removeItem(key);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
localStorage.setItem(key, state);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getPersistedAuthState(params: { configId: string }): PersistedAuthState | undefined {
|
|
25
|
+
const { configId } = params;
|
|
26
|
+
|
|
27
|
+
const value = localStorage.getItem(getKey({ configId }));
|
|
28
|
+
|
|
29
|
+
if (value === null) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
assert(value === "logged in" || value === "explicitly logged out");
|
|
34
|
+
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { assert, typeGuard, id } from "../vendor/frontend/tsafe";
|
|
2
|
+
|
|
3
|
+
type SessionStorageItem_Parsed = {
|
|
4
|
+
__brand: typeof SessionStorageItem_Parsed.brand;
|
|
5
|
+
value: string;
|
|
6
|
+
expiresAtTime: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
namespace SessionStorageItem_Parsed {
|
|
10
|
+
export const brand = "SessionStorageItem_Parsed";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseSessionStorageItem(
|
|
14
|
+
sessionStorageItemValue: string
|
|
15
|
+
): SessionStorageItem_Parsed | undefined {
|
|
16
|
+
let json: unknown;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
json = JSON.parse(sessionStorageItemValue);
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
!typeGuard<SessionStorageItem_Parsed>(
|
|
26
|
+
json,
|
|
27
|
+
json instanceof Object &&
|
|
28
|
+
"__brand" in json &&
|
|
29
|
+
json.__brand === SessionStorageItem_Parsed.brand
|
|
30
|
+
)
|
|
31
|
+
) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return json;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type InMemoryItem = {
|
|
39
|
+
key: string;
|
|
40
|
+
value: string;
|
|
41
|
+
removeFromSessionStorage: (() => void) | undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const SESSION_STORAGE_PREFIX = "ephemeral:";
|
|
45
|
+
|
|
46
|
+
function createStoreInSessionStorageAndScheduleRemovalInMemoryItem(params: {
|
|
47
|
+
key: string;
|
|
48
|
+
value: string;
|
|
49
|
+
remainingTtlMs: number;
|
|
50
|
+
}): InMemoryItem {
|
|
51
|
+
const { key, value, remainingTtlMs } = params;
|
|
52
|
+
|
|
53
|
+
const sessionStorageKey = `${SESSION_STORAGE_PREFIX}${key}`;
|
|
54
|
+
|
|
55
|
+
const removeFromSessionStorage = () => {
|
|
56
|
+
inMemoryItem.removeFromSessionStorage = undefined;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
sessionStorage.removeItem(sessionStorageKey);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const timer = setTimeout(() => removeFromSessionStorage(), remainingTtlMs);
|
|
62
|
+
|
|
63
|
+
const inMemoryItem: InMemoryItem = {
|
|
64
|
+
key,
|
|
65
|
+
value,
|
|
66
|
+
removeFromSessionStorage
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
sessionStorage.removeItem(sessionStorageKey);
|
|
70
|
+
|
|
71
|
+
sessionStorage.setItem(
|
|
72
|
+
sessionStorageKey,
|
|
73
|
+
JSON.stringify(
|
|
74
|
+
id<SessionStorageItem_Parsed>({
|
|
75
|
+
__brand: "SessionStorageItem_Parsed",
|
|
76
|
+
value,
|
|
77
|
+
expiresAtTime: Date.now() + remainingTtlMs
|
|
78
|
+
})
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return inMemoryItem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createEphemeralSessionStorage(params: { sessionStorageTtlMs: number }): Storage {
|
|
86
|
+
const { sessionStorageTtlMs } = params;
|
|
87
|
+
|
|
88
|
+
const inMemoryItems: InMemoryItem[] = [];
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
91
|
+
const sessionStorageKey = sessionStorage.key(i);
|
|
92
|
+
assert(sessionStorageKey !== null);
|
|
93
|
+
|
|
94
|
+
if (!sessionStorageKey.startsWith(SESSION_STORAGE_PREFIX)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sessionStorageItem = sessionStorage.getItem(sessionStorageKey);
|
|
99
|
+
|
|
100
|
+
assert(sessionStorageItem !== null);
|
|
101
|
+
|
|
102
|
+
const sessionStorageItem_parsed = parseSessionStorageItem(sessionStorageItem);
|
|
103
|
+
|
|
104
|
+
if (sessionStorageItem_parsed === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const remainingTtlMs = sessionStorageItem_parsed.expiresAtTime - Date.now();
|
|
109
|
+
|
|
110
|
+
if (remainingTtlMs <= 0) {
|
|
111
|
+
sessionStorage.removeItem(sessionStorageKey);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
inMemoryItems.push(
|
|
116
|
+
createStoreInSessionStorageAndScheduleRemovalInMemoryItem({
|
|
117
|
+
key: sessionStorageKey.slice(SESSION_STORAGE_PREFIX.length),
|
|
118
|
+
value: sessionStorageItem_parsed.value,
|
|
119
|
+
remainingTtlMs
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const storage = {
|
|
125
|
+
get length() {
|
|
126
|
+
return inMemoryItems.length;
|
|
127
|
+
},
|
|
128
|
+
key(index: number) {
|
|
129
|
+
const inMemoryItem = inMemoryItems[index];
|
|
130
|
+
|
|
131
|
+
if (inMemoryItem === undefined) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return inMemoryItem.key;
|
|
136
|
+
},
|
|
137
|
+
removeItem(key: string) {
|
|
138
|
+
const inMemoryItem = inMemoryItems.find(item => item.key === key);
|
|
139
|
+
|
|
140
|
+
if (inMemoryItem === undefined) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
inMemoryItem.removeFromSessionStorage?.();
|
|
145
|
+
|
|
146
|
+
const index = inMemoryItems.indexOf(inMemoryItem);
|
|
147
|
+
|
|
148
|
+
inMemoryItems.splice(index, 1);
|
|
149
|
+
},
|
|
150
|
+
clear() {
|
|
151
|
+
for (let i = 0; i < storage.length; i++) {
|
|
152
|
+
const key = storage.key(i);
|
|
153
|
+
assert(key !== null);
|
|
154
|
+
storage.removeItem(key);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
getItem(key: string) {
|
|
158
|
+
const inMemoryItem = inMemoryItems.find(item => item.key === key);
|
|
159
|
+
if (inMemoryItem === undefined) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return inMemoryItem.value;
|
|
163
|
+
},
|
|
164
|
+
setItem(key: string, value: string) {
|
|
165
|
+
let existingInMemoryItemIndex: number | undefined = undefined;
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
const inMemoryItem = inMemoryItems.find(item => item.key === key);
|
|
169
|
+
|
|
170
|
+
if (inMemoryItem !== undefined) {
|
|
171
|
+
inMemoryItem.removeFromSessionStorage?.();
|
|
172
|
+
existingInMemoryItemIndex = inMemoryItems.indexOf(inMemoryItem);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const inMemoryItem_new = createStoreInSessionStorageAndScheduleRemovalInMemoryItem({
|
|
177
|
+
key,
|
|
178
|
+
value,
|
|
179
|
+
remainingTtlMs: sessionStorageTtlMs
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (existingInMemoryItemIndex !== undefined) {
|
|
183
|
+
inMemoryItems[existingInMemoryItemIndex] = inMemoryItem_new;
|
|
184
|
+
} else {
|
|
185
|
+
inMemoryItems.push(inMemoryItem_new);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return storage;
|
|
191
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function getHaveSharedParentDomain(params: { url1: string; url2: string }): boolean {
|
|
2
|
+
const { url1, url2 } = params;
|
|
3
|
+
|
|
4
|
+
const url1Domain = new URL(url1).hostname;
|
|
5
|
+
const url2Domain = new URL(url2).hostname;
|
|
6
|
+
|
|
7
|
+
const getLevel2Domain = (url: string): string => {
|
|
8
|
+
const parts = url.split(".");
|
|
9
|
+
return parts.slice(parts.length - 2).join(".");
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return getLevel2Domain(url1Domain) === getLevel2Domain(url2Domain);
|
|
13
|
+
}
|