@absolutejs/auth 0.69.0 → 0.69.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.js +510 -509
- package/dist/client/index.js.map +4 -4
- package/dist/client/mobile.js +572 -0
- package/dist/client/mobile.js.map +10 -0
- package/package.json +10 -2
package/dist/client/index.js
CHANGED
|
@@ -46,396 +46,102 @@ var __export = (target, all) => {
|
|
|
46
46
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
47
|
var __require = import.meta.require;
|
|
48
48
|
|
|
49
|
-
// src/client/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
status: "/oauth2/status"
|
|
49
|
+
// src/client/mobile.ts
|
|
50
|
+
class MobileAuthError extends Error {
|
|
51
|
+
code;
|
|
52
|
+
cause;
|
|
53
|
+
constructor(code, message, options) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "MobileAuthError";
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.cause = options?.cause;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var PENDING_KEY = "oidc.pending";
|
|
61
|
+
var REFRESH_KEY = "oidc.refresh";
|
|
62
|
+
var DEFAULT_CLOCK_SKEW_MS = 30000;
|
|
63
|
+
var PENDING_TTL_MS = 10 * 60000;
|
|
64
|
+
var RANDOM_BYTES = 32;
|
|
65
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
var base64Url = (value) => {
|
|
67
|
+
let binary = "";
|
|
68
|
+
for (const byte of value)
|
|
69
|
+
binary += String.fromCharCode(byte);
|
|
70
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
72
71
|
};
|
|
73
|
-
var
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
data: null,
|
|
79
|
-
error
|
|
80
|
-
});
|
|
81
|
-
var errorFor = (response, body) => ({
|
|
82
|
-
body,
|
|
83
|
-
message: typeof body === "string" ? body : readMessage(body) ?? response.statusText,
|
|
84
|
-
status: response.status
|
|
85
|
-
});
|
|
86
|
-
var createAuthClient = ({
|
|
87
|
-
baseUrl = "",
|
|
88
|
-
credentials = "same-origin",
|
|
89
|
-
fetch: fetchImpl = fetch,
|
|
90
|
-
routes,
|
|
91
|
-
transport
|
|
92
|
-
} = {}) => {
|
|
93
|
-
const resolvedFetch = transport?.fetch ?? fetchImpl;
|
|
94
|
-
const signInEmail = transport?.signInEmail;
|
|
95
|
-
const signOut = transport?.signOut;
|
|
96
|
-
const signUpEmail = transport?.signUpEmail;
|
|
97
|
-
const transportStatus = transport?.status;
|
|
98
|
-
const resolvedRoutes = {
|
|
99
|
-
...DEFAULT_ROUTES,
|
|
100
|
-
...routes
|
|
101
|
-
};
|
|
102
|
-
const request = async (path, init) => {
|
|
103
|
-
try {
|
|
104
|
-
const response = await resolvedFetch(`${baseUrl}${path}`, {
|
|
105
|
-
credentials,
|
|
106
|
-
...init
|
|
107
|
-
});
|
|
108
|
-
const text = await response.text();
|
|
109
|
-
if (!response.ok)
|
|
110
|
-
return fail(errorFor(response, safeJson(text)));
|
|
111
|
-
const data = JSON.parse(text === "" ? "null" : text);
|
|
112
|
-
return succeed(data);
|
|
113
|
-
} catch (caught) {
|
|
114
|
-
const message = caught instanceof Error ? caught.message : "network";
|
|
115
|
-
return fail({ body: null, message, status: 0 });
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const runTransport = async (operation) => {
|
|
119
|
-
try {
|
|
120
|
-
return succeed(await operation());
|
|
121
|
-
} catch (caught) {
|
|
122
|
-
const message = caught instanceof Error ? caught.message : "authentication";
|
|
123
|
-
return fail({ body: null, message, status: 0 });
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
const post = (path, body, method = "POST") => request(path, {
|
|
127
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
128
|
-
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
|
129
|
-
method
|
|
130
|
-
});
|
|
131
|
-
const get = (path) => request(path, { method: "GET" });
|
|
132
|
-
const del = (path) => request(path, { method: "DELETE" });
|
|
133
|
-
return {
|
|
134
|
-
emailVerification: {
|
|
135
|
-
request: (body) => post(resolvedRoutes.emailVerifyRequest, body),
|
|
136
|
-
verify: (body) => post(resolvedRoutes.emailVerify, body)
|
|
137
|
-
},
|
|
138
|
-
mfa: {
|
|
139
|
-
challenge: (body) => post(resolvedRoutes.mfaChallenge, body),
|
|
140
|
-
disable: () => del(resolvedRoutes.mfaManagement),
|
|
141
|
-
setup: () => post(resolvedRoutes.mfaSetup),
|
|
142
|
-
status: () => get(resolvedRoutes.mfaManagement),
|
|
143
|
-
verifySetup: (body) => post(resolvedRoutes.mfaVerifySetup, body)
|
|
144
|
-
},
|
|
145
|
-
passkeys: {
|
|
146
|
-
authenticateOptions: () => post(resolvedRoutes.passkeyAuthenticateOptions),
|
|
147
|
-
authenticateVerify: (response) => post(resolvedRoutes.passkeyAuthenticateVerify, response),
|
|
148
|
-
list: () => get(resolvedRoutes.passkeyList),
|
|
149
|
-
registerOptions: () => post(resolvedRoutes.passkeyRegisterOptions),
|
|
150
|
-
registerVerify: (response) => post(resolvedRoutes.passkeyRegisterVerify, response),
|
|
151
|
-
remove: (credentialId) => del(`${resolvedRoutes.passkeyRemove}/${encodeURIComponent(credentialId)}`)
|
|
152
|
-
},
|
|
153
|
-
passwordless: {
|
|
154
|
-
requestMagicLink: (body) => post(resolvedRoutes.magicLinkRequest, body),
|
|
155
|
-
verifyMagicLink: (body) => post(resolvedRoutes.magicLinkVerify, body)
|
|
156
|
-
},
|
|
157
|
-
passwordReset: {
|
|
158
|
-
confirm: (body) => post(resolvedRoutes.passwordReset, body),
|
|
159
|
-
request: (body) => post(resolvedRoutes.passwordResetRequest, body)
|
|
160
|
-
},
|
|
161
|
-
sessions: {
|
|
162
|
-
list: () => get(resolvedRoutes.sessions),
|
|
163
|
-
revoke: (sessionId) => del(`${resolvedRoutes.sessions}/${encodeURIComponent(sessionId)}`)
|
|
164
|
-
},
|
|
165
|
-
signIn: {
|
|
166
|
-
email: (body) => signInEmail ? runTransport(() => signInEmail(body)) : post(resolvedRoutes.login, body)
|
|
167
|
-
},
|
|
168
|
-
signUp: {
|
|
169
|
-
email: (body) => signUpEmail ? runTransport(() => signUpEmail(body)) : post(resolvedRoutes.register, body)
|
|
170
|
-
},
|
|
171
|
-
signOut: () => signOut ? runTransport(() => signOut()) : del(resolvedRoutes.signout),
|
|
172
|
-
status: () => transportStatus ? runTransport(() => transportStatus()) : get(resolvedRoutes.status)
|
|
173
|
-
};
|
|
72
|
+
var decodeBase64Url = (value) => {
|
|
73
|
+
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
74
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
75
|
+
const binary = atob(padded);
|
|
76
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
174
77
|
};
|
|
175
|
-
var
|
|
78
|
+
var randomValue = () => {
|
|
79
|
+
const value = new Uint8Array(RANDOM_BYTES);
|
|
80
|
+
crypto.getRandomValues(value);
|
|
81
|
+
return base64Url(value);
|
|
82
|
+
};
|
|
83
|
+
var pkceChallenge = async (verifier) => base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
|
|
84
|
+
var normalizeIssuer = (value) => {
|
|
85
|
+
const issuer = new URL(value);
|
|
86
|
+
const loopback = issuer.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(issuer.hostname);
|
|
87
|
+
if (issuer.protocol !== "https:" && !loopback)
|
|
88
|
+
throw new TypeError("Mobile auth issuer must use HTTPS.");
|
|
89
|
+
if (issuer.username || issuer.password || issuer.search || issuer.hash)
|
|
90
|
+
throw new TypeError("Mobile auth issuer cannot contain credentials, query, or fragment.");
|
|
91
|
+
issuer.pathname = issuer.pathname.replace(/\/$/u, "");
|
|
92
|
+
return issuer.href.replace(/\/$/u, "");
|
|
93
|
+
};
|
|
94
|
+
var exactRedirect = (actualValue, expectedValue) => {
|
|
95
|
+
const actual = new URL(actualValue);
|
|
96
|
+
const expected = new URL(expectedValue);
|
|
97
|
+
return actual.protocol === expected.protocol && actual.host === expected.host && actual.pathname === expected.pathname && actual.username === "" && actual.password === "";
|
|
98
|
+
};
|
|
99
|
+
var parsePending = (value) => {
|
|
100
|
+
if (value === null)
|
|
101
|
+
return;
|
|
176
102
|
try {
|
|
177
|
-
|
|
103
|
+
const parsed = JSON.parse(value);
|
|
104
|
+
if (!isRecord(parsed) || typeof parsed.createdAt !== "number" || typeof parsed.nonce !== "string" || typeof parsed.state !== "string" || typeof parsed.verifier !== "string")
|
|
105
|
+
return;
|
|
106
|
+
return {
|
|
107
|
+
createdAt: parsed.createdAt,
|
|
108
|
+
nonce: parsed.nonce,
|
|
109
|
+
state: parsed.state,
|
|
110
|
+
verifier: parsed.verifier
|
|
111
|
+
};
|
|
178
112
|
} catch {
|
|
179
|
-
return
|
|
113
|
+
return;
|
|
180
114
|
}
|
|
181
115
|
};
|
|
182
|
-
var
|
|
183
|
-
if (typeof
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
116
|
+
var parseTokenResponse = (value) => {
|
|
117
|
+
if (!isRecord(value) || typeof value.access_token !== "string" || typeof value.expires_in !== "number" || !Number.isFinite(value.expires_in) || value.expires_in <= 0 || typeof value.id_token !== "string" || typeof value.refresh_token !== "string" || typeof value.token_type !== "string")
|
|
118
|
+
throw new MobileAuthError("token", "The token response is malformed.");
|
|
119
|
+
if (value.token_type.toLowerCase() !== "bearer")
|
|
120
|
+
throw new MobileAuthError("token", `Unsupported mobile token type ${value.token_type}; DPoP is not enabled for this client.`);
|
|
121
|
+
return {
|
|
122
|
+
access_token: value.access_token,
|
|
123
|
+
expires_in: value.expires_in,
|
|
124
|
+
id_token: value.id_token,
|
|
125
|
+
refresh_token: value.refresh_token,
|
|
126
|
+
scope: typeof value.scope === "string" ? value.scope : undefined,
|
|
127
|
+
token_type: value.token_type
|
|
128
|
+
};
|
|
187
129
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
var DEFAULT_REASON = "session_expired";
|
|
191
|
-
var DEFAULT_REASON_PARAM = "reason";
|
|
192
|
-
var DEFAULT_RETURN_URL_PARAM = "returnUrl";
|
|
193
|
-
var DEFAULT_SIGN_IN_PATH = "/signin";
|
|
194
|
-
var DEFAULT_STATUS_PATH = "/oauth2/status";
|
|
195
|
-
var HTTP_UNAUTHORIZED = 401;
|
|
196
|
-
var activeGuard = null;
|
|
197
|
-
var requestUrl = (input, origin) => {
|
|
198
|
-
const raw = input instanceof Request ? input.url : String(input);
|
|
130
|
+
var responseBody = async (response) => {
|
|
131
|
+
const text = await response.text();
|
|
199
132
|
try {
|
|
200
|
-
return
|
|
133
|
+
return JSON.parse(text);
|
|
201
134
|
} catch {
|
|
202
|
-
return
|
|
135
|
+
return text;
|
|
203
136
|
}
|
|
204
137
|
};
|
|
205
|
-
var
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const current = new URL(currentHref);
|
|
213
|
-
const returnTo = `${current.pathname}${current.search}${current.hash}`;
|
|
214
|
-
const destination = new URL(signInPath, current.origin);
|
|
215
|
-
destination.searchParams.set(reasonParam, reason);
|
|
216
|
-
destination.searchParams.set(returnUrlParam, returnTo);
|
|
217
|
-
return destination.origin === current.origin ? `${destination.pathname}${destination.search}${destination.hash}` : destination.toString();
|
|
218
|
-
};
|
|
219
|
-
var installSessionExpiryGuard = (config = {}) => {
|
|
220
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
221
|
-
return { check: async () => false, dispose: () => {
|
|
222
|
-
return;
|
|
223
|
-
} };
|
|
224
|
-
}
|
|
225
|
-
if (activeGuard)
|
|
226
|
-
return activeGuard;
|
|
227
|
-
const {
|
|
228
|
-
checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS,
|
|
229
|
-
isProtectedRequest,
|
|
230
|
-
onExpired,
|
|
231
|
-
protectedPaths = [],
|
|
232
|
-
reason = DEFAULT_REASON,
|
|
233
|
-
reasonParam = DEFAULT_REASON_PARAM,
|
|
234
|
-
returnUrlParam = DEFAULT_RETURN_URL_PARAM,
|
|
235
|
-
signInPath = DEFAULT_SIGN_IN_PATH,
|
|
236
|
-
statusPath = DEFAULT_STATUS_PATH
|
|
237
|
-
} = config;
|
|
238
|
-
const nativeFetch = window.fetch.bind(window);
|
|
239
|
-
const originalFetch = window.fetch;
|
|
240
|
-
let checking = false;
|
|
241
|
-
let disposed = false;
|
|
242
|
-
let expired = false;
|
|
243
|
-
let lastCheckedAt = Date.now();
|
|
244
|
-
const expire = () => {
|
|
245
|
-
if (expired || disposed)
|
|
246
|
-
return;
|
|
247
|
-
expired = true;
|
|
248
|
-
const currentHref = window.location.href;
|
|
249
|
-
const signInUrl = buildSessionExpiredSignInUrl({
|
|
250
|
-
currentHref,
|
|
251
|
-
reason,
|
|
252
|
-
reasonParam,
|
|
253
|
-
returnUrlParam,
|
|
254
|
-
signInPath
|
|
255
|
-
});
|
|
256
|
-
const current = new URL(currentHref);
|
|
257
|
-
const returnTo = `${current.pathname}${current.search}${current.hash}`;
|
|
258
|
-
if (onExpired) {
|
|
259
|
-
onExpired({ returnTo, signInUrl });
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
window.location.assign(signInUrl);
|
|
263
|
-
};
|
|
264
|
-
const protectedRequest = (input) => {
|
|
265
|
-
const url = requestUrl(input, window.location.origin);
|
|
266
|
-
if (!url || url.origin !== window.location.origin)
|
|
267
|
-
return false;
|
|
268
|
-
if (new URL(statusPath, window.location.origin).pathname === url.pathname)
|
|
269
|
-
return false;
|
|
270
|
-
return isProtectedRequest?.(url) === true || protectedPaths.some((path) => url.pathname.startsWith(path));
|
|
271
|
-
};
|
|
272
|
-
const guardedFetch = new Proxy(originalFetch, {
|
|
273
|
-
apply: async (target, thisArg, args) => {
|
|
274
|
-
const [input] = args;
|
|
275
|
-
const response = await Reflect.apply(target, thisArg, args);
|
|
276
|
-
if (response.status === HTTP_UNAUTHORIZED && protectedRequest(input)) {
|
|
277
|
-
expire();
|
|
278
|
-
}
|
|
279
|
-
return response;
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
window.fetch = guardedFetch;
|
|
283
|
-
const check = async () => {
|
|
284
|
-
if (checking || expired || disposed)
|
|
285
|
-
return false;
|
|
286
|
-
checking = true;
|
|
287
|
-
lastCheckedAt = Date.now();
|
|
288
|
-
try {
|
|
289
|
-
const response = await nativeFetch(statusPath, {
|
|
290
|
-
cache: "no-store",
|
|
291
|
-
credentials: "include",
|
|
292
|
-
headers: { accept: "application/json" }
|
|
293
|
-
});
|
|
294
|
-
if (!response.ok)
|
|
295
|
-
return false;
|
|
296
|
-
const payload = await response.json();
|
|
297
|
-
const sessionExpired = typeof payload === "object" && payload !== null && Reflect.get(payload, "user") === null;
|
|
298
|
-
if (!sessionExpired)
|
|
299
|
-
return false;
|
|
300
|
-
expire();
|
|
301
|
-
return true;
|
|
302
|
-
} catch {
|
|
303
|
-
return false;
|
|
304
|
-
} finally {
|
|
305
|
-
checking = false;
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
const checkIfDue = () => {
|
|
309
|
-
if (document.visibilityState !== "visible" || Date.now() - lastCheckedAt < checkIntervalMs)
|
|
310
|
-
return;
|
|
311
|
-
check();
|
|
312
|
-
};
|
|
313
|
-
const checkPersistedSession = (event) => {
|
|
314
|
-
if (!event.persisted)
|
|
315
|
-
return;
|
|
316
|
-
check();
|
|
317
|
-
};
|
|
318
|
-
const dispose = () => {
|
|
319
|
-
if (disposed)
|
|
320
|
-
return;
|
|
321
|
-
disposed = true;
|
|
322
|
-
document.removeEventListener("visibilitychange", checkIfDue);
|
|
323
|
-
window.removeEventListener("focus", checkIfDue);
|
|
324
|
-
window.removeEventListener("pageshow", checkPersistedSession);
|
|
325
|
-
if (window.fetch === guardedFetch)
|
|
326
|
-
window.fetch = originalFetch;
|
|
327
|
-
activeGuard = null;
|
|
328
|
-
};
|
|
329
|
-
document.addEventListener("visibilitychange", checkIfDue);
|
|
330
|
-
window.addEventListener("focus", checkIfDue);
|
|
331
|
-
window.addEventListener("pageshow", checkPersistedSession);
|
|
332
|
-
activeGuard = { check, dispose };
|
|
333
|
-
return activeGuard;
|
|
334
|
-
};
|
|
335
|
-
var isProtectedSessionRequest = ({
|
|
336
|
-
input,
|
|
337
|
-
origin,
|
|
338
|
-
protectedPaths
|
|
339
|
-
}) => {
|
|
340
|
-
const url = requestUrl(input, origin);
|
|
341
|
-
return url?.origin === origin && protectedPaths.some((path) => url.pathname.startsWith(path));
|
|
342
|
-
};
|
|
343
|
-
// src/client/mobile.ts
|
|
344
|
-
class MobileAuthError extends Error {
|
|
345
|
-
code;
|
|
346
|
-
cause;
|
|
347
|
-
constructor(code, message, options) {
|
|
348
|
-
super(message);
|
|
349
|
-
this.name = "MobileAuthError";
|
|
350
|
-
this.code = code;
|
|
351
|
-
this.cause = options?.cause;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
var PENDING_KEY = "oidc.pending";
|
|
355
|
-
var REFRESH_KEY = "oidc.refresh";
|
|
356
|
-
var DEFAULT_CLOCK_SKEW_MS = 30000;
|
|
357
|
-
var PENDING_TTL_MS = 10 * 60000;
|
|
358
|
-
var RANDOM_BYTES = 32;
|
|
359
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
360
|
-
var base64Url = (value) => {
|
|
361
|
-
let binary = "";
|
|
362
|
-
for (const byte of value)
|
|
363
|
-
binary += String.fromCharCode(byte);
|
|
364
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
365
|
-
};
|
|
366
|
-
var decodeBase64Url = (value) => {
|
|
367
|
-
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
368
|
-
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
369
|
-
const binary = atob(padded);
|
|
370
|
-
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
371
|
-
};
|
|
372
|
-
var randomValue = () => {
|
|
373
|
-
const value = new Uint8Array(RANDOM_BYTES);
|
|
374
|
-
crypto.getRandomValues(value);
|
|
375
|
-
return base64Url(value);
|
|
376
|
-
};
|
|
377
|
-
var pkceChallenge = async (verifier) => base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
|
|
378
|
-
var normalizeIssuer = (value) => {
|
|
379
|
-
const issuer = new URL(value);
|
|
380
|
-
const loopback = issuer.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(issuer.hostname);
|
|
381
|
-
if (issuer.protocol !== "https:" && !loopback)
|
|
382
|
-
throw new TypeError("Mobile auth issuer must use HTTPS.");
|
|
383
|
-
if (issuer.username || issuer.password || issuer.search || issuer.hash)
|
|
384
|
-
throw new TypeError("Mobile auth issuer cannot contain credentials, query, or fragment.");
|
|
385
|
-
issuer.pathname = issuer.pathname.replace(/\/$/u, "");
|
|
386
|
-
return issuer.href.replace(/\/$/u, "");
|
|
387
|
-
};
|
|
388
|
-
var exactRedirect = (actualValue, expectedValue) => {
|
|
389
|
-
const actual = new URL(actualValue);
|
|
390
|
-
const expected = new URL(expectedValue);
|
|
391
|
-
return actual.protocol === expected.protocol && actual.host === expected.host && actual.pathname === expected.pathname && actual.username === "" && actual.password === "";
|
|
392
|
-
};
|
|
393
|
-
var parsePending = (value) => {
|
|
394
|
-
if (value === null)
|
|
395
|
-
return;
|
|
396
|
-
try {
|
|
397
|
-
const parsed = JSON.parse(value);
|
|
398
|
-
if (!isRecord(parsed) || typeof parsed.createdAt !== "number" || typeof parsed.nonce !== "string" || typeof parsed.state !== "string" || typeof parsed.verifier !== "string")
|
|
399
|
-
return;
|
|
400
|
-
return {
|
|
401
|
-
createdAt: parsed.createdAt,
|
|
402
|
-
nonce: parsed.nonce,
|
|
403
|
-
state: parsed.state,
|
|
404
|
-
verifier: parsed.verifier
|
|
405
|
-
};
|
|
406
|
-
} catch {
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
var parseTokenResponse = (value) => {
|
|
411
|
-
if (!isRecord(value) || typeof value.access_token !== "string" || typeof value.expires_in !== "number" || !Number.isFinite(value.expires_in) || value.expires_in <= 0 || typeof value.id_token !== "string" || typeof value.refresh_token !== "string" || typeof value.token_type !== "string")
|
|
412
|
-
throw new MobileAuthError("token", "The token response is malformed.");
|
|
413
|
-
if (value.token_type.toLowerCase() !== "bearer")
|
|
414
|
-
throw new MobileAuthError("token", `Unsupported mobile token type ${value.token_type}; DPoP is not enabled for this client.`);
|
|
415
|
-
return {
|
|
416
|
-
access_token: value.access_token,
|
|
417
|
-
expires_in: value.expires_in,
|
|
418
|
-
id_token: value.id_token,
|
|
419
|
-
refresh_token: value.refresh_token,
|
|
420
|
-
scope: typeof value.scope === "string" ? value.scope : undefined,
|
|
421
|
-
token_type: value.token_type
|
|
422
|
-
};
|
|
423
|
-
};
|
|
424
|
-
var responseBody = async (response) => {
|
|
425
|
-
const text = await response.text();
|
|
426
|
-
try {
|
|
427
|
-
return JSON.parse(text);
|
|
428
|
-
} catch {
|
|
429
|
-
return text;
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
var requireEndpoint = (value, name, issuer) => {
|
|
433
|
-
if (typeof value !== "string")
|
|
434
|
-
throw new MobileAuthError("discovery", `OIDC discovery is missing ${name}.`);
|
|
435
|
-
const endpoint = new URL(value);
|
|
436
|
-
if (endpoint.protocol !== "https:" && new URL(issuer).protocol === "https:")
|
|
437
|
-
throw new MobileAuthError("discovery", `OIDC discovery ${name} must use HTTPS.`);
|
|
438
|
-
return endpoint.href;
|
|
138
|
+
var requireEndpoint = (value, name, issuer) => {
|
|
139
|
+
if (typeof value !== "string")
|
|
140
|
+
throw new MobileAuthError("discovery", `OIDC discovery is missing ${name}.`);
|
|
141
|
+
const endpoint = new URL(value);
|
|
142
|
+
if (endpoint.protocol !== "https:" && new URL(issuer).protocol === "https:")
|
|
143
|
+
throw new MobileAuthError("discovery", `OIDC discovery ${name} must use HTTPS.`);
|
|
144
|
+
return endpoint.href;
|
|
439
145
|
};
|
|
440
146
|
var parseDiscovery = (value, issuer) => {
|
|
441
147
|
if (!isRecord(value) || value.issuer !== issuer)
|
|
@@ -716,146 +422,441 @@ var createMobileAuthClient = (config) => {
|
|
|
716
422
|
reject(new MobileAuthError("aborted", "Authorization was cancelled."));
|
|
717
423
|
}, { once: true });
|
|
718
424
|
});
|
|
719
|
-
try {
|
|
720
|
-
await config.links.openExternal(url.href);
|
|
721
|
-
} catch (error) {
|
|
722
|
-
pendingSignIns.delete(pending.state);
|
|
723
|
-
await config.storage.remove(PENDING_KEY);
|
|
724
|
-
throw error;
|
|
425
|
+
try {
|
|
426
|
+
await config.links.openExternal(url.href);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
pendingSignIns.delete(pending.state);
|
|
429
|
+
await config.storage.remove(PENDING_KEY);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
};
|
|
434
|
+
const authenticatedFetch = async (input, init) => {
|
|
435
|
+
const original = new Request(input, init);
|
|
436
|
+
if (!allowedOrigins.has(new URL(original.url).origin))
|
|
437
|
+
throw new MobileAuthError("origin", "Mobile auth refused to send a credential to an unregistered origin.");
|
|
438
|
+
const send = async (forceRefresh) => {
|
|
439
|
+
if (forceRefresh)
|
|
440
|
+
access = undefined;
|
|
441
|
+
const token = await refreshAccessToken();
|
|
442
|
+
const request = original.clone();
|
|
443
|
+
const headers = new Headers(request.headers);
|
|
444
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
445
|
+
return fetchImpl(new Request(request, { credentials: "omit", headers }));
|
|
446
|
+
};
|
|
447
|
+
const response = await send(false);
|
|
448
|
+
return response.status === 401 ? send(true) : response;
|
|
449
|
+
};
|
|
450
|
+
const optionalAuthenticatedFetch = async (input, init) => {
|
|
451
|
+
const request = new Request(input, init);
|
|
452
|
+
if (!allowedOrigins.has(new URL(request.url).origin))
|
|
453
|
+
throw new MobileAuthError("origin", "Mobile auth refused to send a request outside an allowed origin.");
|
|
454
|
+
const refreshToken = await config.storage.get(REFRESH_KEY);
|
|
455
|
+
if (access || refreshToken)
|
|
456
|
+
return authenticatedFetch(request);
|
|
457
|
+
return fetchImpl(new Request(request, { credentials: "omit" }));
|
|
458
|
+
};
|
|
459
|
+
const status = async () => {
|
|
460
|
+
try {
|
|
461
|
+
const metadata = await discovery();
|
|
462
|
+
const response = await authenticatedFetch(metadata.userinfo_endpoint);
|
|
463
|
+
if (response.status === 401)
|
|
464
|
+
return null;
|
|
465
|
+
if (!response.ok)
|
|
466
|
+
throw new MobileAuthError("network", `User info failed with HTTP ${response.status}.`);
|
|
467
|
+
const user = await response.json();
|
|
468
|
+
if (!isRecord(user) || typeof user.sub !== "string")
|
|
469
|
+
throw new MobileAuthError("token", "The user-info response is malformed.");
|
|
470
|
+
return { ...user, sub: user.sub };
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (error instanceof MobileAuthError && (error.code === "oauth" || error.code === "token"))
|
|
473
|
+
return null;
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const socketTicket = async (audience = resource) => {
|
|
478
|
+
const metadata = await discovery();
|
|
479
|
+
if (!metadata.socket_ticket_endpoint)
|
|
480
|
+
throw new MobileAuthError("discovery", "The authorization server does not advertise WebSocket tickets.");
|
|
481
|
+
const response = await authenticatedFetch(metadata.socket_ticket_endpoint, {
|
|
482
|
+
body: JSON.stringify({ audience }),
|
|
483
|
+
headers: { "content-type": "application/json" },
|
|
484
|
+
method: "POST"
|
|
485
|
+
});
|
|
486
|
+
const body = await responseBody(response);
|
|
487
|
+
if (!response.ok || !isRecord(body) || typeof body.ticket !== "string")
|
|
488
|
+
throw new MobileAuthError(response.ok ? "token" : "oauth", `WebSocket ticket request failed with HTTP ${response.status}.`);
|
|
489
|
+
return body.ticket;
|
|
490
|
+
};
|
|
491
|
+
const revokeRefreshToken = async (refreshToken) => {
|
|
492
|
+
const metadata = await discovery();
|
|
493
|
+
if (metadata.revocation_endpoint)
|
|
494
|
+
await fetchImpl(metadata.revocation_endpoint, {
|
|
495
|
+
body: new URLSearchParams({
|
|
496
|
+
client_id: config.clientId,
|
|
497
|
+
token: refreshToken,
|
|
498
|
+
token_type_hint: "refresh_token"
|
|
499
|
+
}),
|
|
500
|
+
credentials: "omit",
|
|
501
|
+
headers: {
|
|
502
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
503
|
+
},
|
|
504
|
+
method: "POST"
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
const signOut = async () => {
|
|
508
|
+
const refreshToken = await config.storage.get(REFRESH_KEY);
|
|
509
|
+
try {
|
|
510
|
+
if (refreshToken)
|
|
511
|
+
await revokeRefreshToken(refreshToken);
|
|
512
|
+
} finally {
|
|
513
|
+
access = undefined;
|
|
514
|
+
await Promise.all([
|
|
515
|
+
config.storage.remove(PENDING_KEY),
|
|
516
|
+
config.storage.remove(REFRESH_KEY)
|
|
517
|
+
]);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
const stop = async () => {
|
|
521
|
+
await Promise.all([stopLinks?.(), stopResume?.()]);
|
|
522
|
+
stopLinks = undefined;
|
|
523
|
+
stopResume = undefined;
|
|
524
|
+
startPromise = undefined;
|
|
525
|
+
};
|
|
526
|
+
return {
|
|
527
|
+
fetch: authenticatedFetch,
|
|
528
|
+
fetchOptional: optionalAuthenticatedFetch,
|
|
529
|
+
handleCallback,
|
|
530
|
+
refresh: refreshAccessToken,
|
|
531
|
+
signIn,
|
|
532
|
+
signOut,
|
|
533
|
+
socketTicket,
|
|
534
|
+
start,
|
|
535
|
+
status,
|
|
536
|
+
stop
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
var createMobileAuthTransport = (client) => ({
|
|
540
|
+
fetch: client.fetch,
|
|
541
|
+
signInEmail: async ({ email }) => {
|
|
542
|
+
await client.signIn({
|
|
543
|
+
authorizationParameters: { login_hint: email }
|
|
544
|
+
});
|
|
545
|
+
return { status: "authenticated" };
|
|
546
|
+
},
|
|
547
|
+
signOut: async () => {
|
|
548
|
+
await client.signOut();
|
|
549
|
+
return null;
|
|
550
|
+
},
|
|
551
|
+
signUpEmail: async ({ email }) => {
|
|
552
|
+
await client.signIn({
|
|
553
|
+
authorizationParameters: {
|
|
554
|
+
login_hint: email,
|
|
555
|
+
screen_hint: "signup"
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
return { status: "authenticated" };
|
|
559
|
+
},
|
|
560
|
+
status: async () => {
|
|
561
|
+
const user = await client.status();
|
|
562
|
+
return { user };
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// src/client/createAuthClient.ts
|
|
567
|
+
var DEFAULT_ROUTES = {
|
|
568
|
+
emailVerify: "/auth/verify-email",
|
|
569
|
+
emailVerifyRequest: "/auth/verify-email/request",
|
|
570
|
+
login: "/auth/login",
|
|
571
|
+
magicLinkRequest: "/auth/passwordless/magic-link",
|
|
572
|
+
magicLinkVerify: "/auth/passwordless/magic-link/verify",
|
|
573
|
+
mfaChallenge: "/auth/mfa/totp/challenge",
|
|
574
|
+
mfaManagement: "/auth/mfa",
|
|
575
|
+
mfaSetup: "/auth/mfa/totp/setup",
|
|
576
|
+
mfaVerifySetup: "/auth/mfa/totp/verify",
|
|
577
|
+
passkeyAuthenticateOptions: "/auth/webauthn/authenticate/options",
|
|
578
|
+
passkeyAuthenticateVerify: "/auth/webauthn/authenticate/verify",
|
|
579
|
+
passkeyList: "/auth/webauthn/credentials",
|
|
580
|
+
passkeyRegisterOptions: "/auth/webauthn/register/options",
|
|
581
|
+
passkeyRegisterVerify: "/auth/webauthn/register/verify",
|
|
582
|
+
passkeyRemove: "/auth/webauthn/credentials",
|
|
583
|
+
passwordReset: "/auth/reset-password",
|
|
584
|
+
passwordResetRequest: "/auth/reset-password/request",
|
|
585
|
+
register: "/auth/register",
|
|
586
|
+
sessions: "/auth/sessions",
|
|
587
|
+
signout: "/oauth2/signout",
|
|
588
|
+
status: "/oauth2/status"
|
|
589
|
+
};
|
|
590
|
+
var succeed = (data) => ({
|
|
591
|
+
data,
|
|
592
|
+
error: null
|
|
593
|
+
});
|
|
594
|
+
var fail = (error) => ({
|
|
595
|
+
data: null,
|
|
596
|
+
error
|
|
597
|
+
});
|
|
598
|
+
var errorFor = (response, body) => ({
|
|
599
|
+
body,
|
|
600
|
+
message: typeof body === "string" ? body : readMessage(body) ?? response.statusText,
|
|
601
|
+
status: response.status
|
|
602
|
+
});
|
|
603
|
+
var createAuthClient = ({
|
|
604
|
+
baseUrl = "",
|
|
605
|
+
credentials = "same-origin",
|
|
606
|
+
fetch: fetchImpl = fetch,
|
|
607
|
+
routes,
|
|
608
|
+
transport
|
|
609
|
+
} = {}) => {
|
|
610
|
+
const resolvedFetch = transport?.fetch ?? fetchImpl;
|
|
611
|
+
const signInEmail = transport?.signInEmail;
|
|
612
|
+
const signOut = transport?.signOut;
|
|
613
|
+
const signUpEmail = transport?.signUpEmail;
|
|
614
|
+
const transportStatus = transport?.status;
|
|
615
|
+
const resolvedRoutes = {
|
|
616
|
+
...DEFAULT_ROUTES,
|
|
617
|
+
...routes
|
|
618
|
+
};
|
|
619
|
+
const request = async (path, init) => {
|
|
620
|
+
try {
|
|
621
|
+
const response = await resolvedFetch(`${baseUrl}${path}`, {
|
|
622
|
+
credentials,
|
|
623
|
+
...init
|
|
624
|
+
});
|
|
625
|
+
const text = await response.text();
|
|
626
|
+
if (!response.ok)
|
|
627
|
+
return fail(errorFor(response, safeJson(text)));
|
|
628
|
+
const data = JSON.parse(text === "" ? "null" : text);
|
|
629
|
+
return succeed(data);
|
|
630
|
+
} catch (caught) {
|
|
631
|
+
const message = caught instanceof Error ? caught.message : "network";
|
|
632
|
+
return fail({ body: null, message, status: 0 });
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
const runTransport = async (operation) => {
|
|
636
|
+
try {
|
|
637
|
+
return succeed(await operation());
|
|
638
|
+
} catch (caught) {
|
|
639
|
+
const message = caught instanceof Error ? caught.message : "authentication";
|
|
640
|
+
return fail({ body: null, message, status: 0 });
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const post = (path, body, method = "POST") => request(path, {
|
|
644
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
645
|
+
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
|
646
|
+
method
|
|
647
|
+
});
|
|
648
|
+
const get = (path) => request(path, { method: "GET" });
|
|
649
|
+
const del = (path) => request(path, { method: "DELETE" });
|
|
650
|
+
return {
|
|
651
|
+
emailVerification: {
|
|
652
|
+
request: (body) => post(resolvedRoutes.emailVerifyRequest, body),
|
|
653
|
+
verify: (body) => post(resolvedRoutes.emailVerify, body)
|
|
654
|
+
},
|
|
655
|
+
mfa: {
|
|
656
|
+
challenge: (body) => post(resolvedRoutes.mfaChallenge, body),
|
|
657
|
+
disable: () => del(resolvedRoutes.mfaManagement),
|
|
658
|
+
setup: () => post(resolvedRoutes.mfaSetup),
|
|
659
|
+
status: () => get(resolvedRoutes.mfaManagement),
|
|
660
|
+
verifySetup: (body) => post(resolvedRoutes.mfaVerifySetup, body)
|
|
661
|
+
},
|
|
662
|
+
passkeys: {
|
|
663
|
+
authenticateOptions: () => post(resolvedRoutes.passkeyAuthenticateOptions),
|
|
664
|
+
authenticateVerify: (response) => post(resolvedRoutes.passkeyAuthenticateVerify, response),
|
|
665
|
+
list: () => get(resolvedRoutes.passkeyList),
|
|
666
|
+
registerOptions: () => post(resolvedRoutes.passkeyRegisterOptions),
|
|
667
|
+
registerVerify: (response) => post(resolvedRoutes.passkeyRegisterVerify, response),
|
|
668
|
+
remove: (credentialId) => del(`${resolvedRoutes.passkeyRemove}/${encodeURIComponent(credentialId)}`)
|
|
669
|
+
},
|
|
670
|
+
passwordless: {
|
|
671
|
+
requestMagicLink: (body) => post(resolvedRoutes.magicLinkRequest, body),
|
|
672
|
+
verifyMagicLink: (body) => post(resolvedRoutes.magicLinkVerify, body)
|
|
673
|
+
},
|
|
674
|
+
passwordReset: {
|
|
675
|
+
confirm: (body) => post(resolvedRoutes.passwordReset, body),
|
|
676
|
+
request: (body) => post(resolvedRoutes.passwordResetRequest, body)
|
|
677
|
+
},
|
|
678
|
+
sessions: {
|
|
679
|
+
list: () => get(resolvedRoutes.sessions),
|
|
680
|
+
revoke: (sessionId) => del(`${resolvedRoutes.sessions}/${encodeURIComponent(sessionId)}`)
|
|
681
|
+
},
|
|
682
|
+
signIn: {
|
|
683
|
+
email: (body) => signInEmail ? runTransport(() => signInEmail(body)) : post(resolvedRoutes.login, body)
|
|
684
|
+
},
|
|
685
|
+
signUp: {
|
|
686
|
+
email: (body) => signUpEmail ? runTransport(() => signUpEmail(body)) : post(resolvedRoutes.register, body)
|
|
687
|
+
},
|
|
688
|
+
signOut: () => signOut ? runTransport(() => signOut()) : del(resolvedRoutes.signout),
|
|
689
|
+
status: () => transportStatus ? runTransport(() => transportStatus()) : get(resolvedRoutes.status)
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
var safeJson = (text) => {
|
|
693
|
+
try {
|
|
694
|
+
return JSON.parse(text);
|
|
695
|
+
} catch {
|
|
696
|
+
return text;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
var readMessage = (body) => {
|
|
700
|
+
if (typeof body !== "object" || body === null)
|
|
701
|
+
return;
|
|
702
|
+
const message = Reflect.get(body, "message");
|
|
703
|
+
return typeof message === "string" ? message : undefined;
|
|
704
|
+
};
|
|
705
|
+
// src/client/sessionExpiry.ts
|
|
706
|
+
var DEFAULT_CHECK_INTERVAL_MS = 30000;
|
|
707
|
+
var DEFAULT_REASON = "session_expired";
|
|
708
|
+
var DEFAULT_REASON_PARAM = "reason";
|
|
709
|
+
var DEFAULT_RETURN_URL_PARAM = "returnUrl";
|
|
710
|
+
var DEFAULT_SIGN_IN_PATH = "/signin";
|
|
711
|
+
var DEFAULT_STATUS_PATH = "/oauth2/status";
|
|
712
|
+
var HTTP_UNAUTHORIZED = 401;
|
|
713
|
+
var activeGuard = null;
|
|
714
|
+
var requestUrl = (input, origin) => {
|
|
715
|
+
const raw = input instanceof Request ? input.url : String(input);
|
|
716
|
+
try {
|
|
717
|
+
return new URL(raw, origin);
|
|
718
|
+
} catch {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
var buildSessionExpiredSignInUrl = ({
|
|
723
|
+
currentHref,
|
|
724
|
+
reason = DEFAULT_REASON,
|
|
725
|
+
reasonParam = DEFAULT_REASON_PARAM,
|
|
726
|
+
returnUrlParam = DEFAULT_RETURN_URL_PARAM,
|
|
727
|
+
signInPath = DEFAULT_SIGN_IN_PATH
|
|
728
|
+
}) => {
|
|
729
|
+
const current = new URL(currentHref);
|
|
730
|
+
const returnTo = `${current.pathname}${current.search}${current.hash}`;
|
|
731
|
+
const destination = new URL(signInPath, current.origin);
|
|
732
|
+
destination.searchParams.set(reasonParam, reason);
|
|
733
|
+
destination.searchParams.set(returnUrlParam, returnTo);
|
|
734
|
+
return destination.origin === current.origin ? `${destination.pathname}${destination.search}${destination.hash}` : destination.toString();
|
|
735
|
+
};
|
|
736
|
+
var installSessionExpiryGuard = (config = {}) => {
|
|
737
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
738
|
+
return { check: async () => false, dispose: () => {
|
|
739
|
+
return;
|
|
740
|
+
} };
|
|
741
|
+
}
|
|
742
|
+
if (activeGuard)
|
|
743
|
+
return activeGuard;
|
|
744
|
+
const {
|
|
745
|
+
checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS,
|
|
746
|
+
isProtectedRequest,
|
|
747
|
+
onExpired,
|
|
748
|
+
protectedPaths = [],
|
|
749
|
+
reason = DEFAULT_REASON,
|
|
750
|
+
reasonParam = DEFAULT_REASON_PARAM,
|
|
751
|
+
returnUrlParam = DEFAULT_RETURN_URL_PARAM,
|
|
752
|
+
signInPath = DEFAULT_SIGN_IN_PATH,
|
|
753
|
+
statusPath = DEFAULT_STATUS_PATH
|
|
754
|
+
} = config;
|
|
755
|
+
const nativeFetch = window.fetch.bind(window);
|
|
756
|
+
const originalFetch = window.fetch;
|
|
757
|
+
let checking = false;
|
|
758
|
+
let disposed = false;
|
|
759
|
+
let expired = false;
|
|
760
|
+
let lastCheckedAt = Date.now();
|
|
761
|
+
const expire = () => {
|
|
762
|
+
if (expired || disposed)
|
|
763
|
+
return;
|
|
764
|
+
expired = true;
|
|
765
|
+
const currentHref = window.location.href;
|
|
766
|
+
const signInUrl = buildSessionExpiredSignInUrl({
|
|
767
|
+
currentHref,
|
|
768
|
+
reason,
|
|
769
|
+
reasonParam,
|
|
770
|
+
returnUrlParam,
|
|
771
|
+
signInPath
|
|
772
|
+
});
|
|
773
|
+
const current = new URL(currentHref);
|
|
774
|
+
const returnTo = `${current.pathname}${current.search}${current.hash}`;
|
|
775
|
+
if (onExpired) {
|
|
776
|
+
onExpired({ returnTo, signInUrl });
|
|
777
|
+
return;
|
|
725
778
|
}
|
|
726
|
-
|
|
727
|
-
};
|
|
728
|
-
const authenticatedFetch = async (input, init) => {
|
|
729
|
-
const original = new Request(input, init);
|
|
730
|
-
if (!allowedOrigins.has(new URL(original.url).origin))
|
|
731
|
-
throw new MobileAuthError("origin", "Mobile auth refused to send a credential to an unregistered origin.");
|
|
732
|
-
const send = async (forceRefresh) => {
|
|
733
|
-
if (forceRefresh)
|
|
734
|
-
access = undefined;
|
|
735
|
-
const token = await refreshAccessToken();
|
|
736
|
-
const request = original.clone();
|
|
737
|
-
const headers = new Headers(request.headers);
|
|
738
|
-
headers.set("authorization", `Bearer ${token}`);
|
|
739
|
-
return fetchImpl(new Request(request, { credentials: "omit", headers }));
|
|
740
|
-
};
|
|
741
|
-
const response = await send(false);
|
|
742
|
-
return response.status === 401 ? send(true) : response;
|
|
779
|
+
window.location.assign(signInUrl);
|
|
743
780
|
};
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
if (!
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
return fetchImpl(new Request(request, { credentials: "omit" }));
|
|
781
|
+
const protectedRequest = (input) => {
|
|
782
|
+
const url = requestUrl(input, window.location.origin);
|
|
783
|
+
if (!url || url.origin !== window.location.origin)
|
|
784
|
+
return false;
|
|
785
|
+
if (new URL(statusPath, window.location.origin).pathname === url.pathname)
|
|
786
|
+
return false;
|
|
787
|
+
return isProtectedRequest?.(url) === true || protectedPaths.some((path) => url.pathname.startsWith(path));
|
|
752
788
|
};
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
const response = await
|
|
757
|
-
if (response.status ===
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const user = await response.json();
|
|
762
|
-
if (!isRecord(user) || typeof user.sub !== "string")
|
|
763
|
-
throw new MobileAuthError("token", "The user-info response is malformed.");
|
|
764
|
-
return { ...user, sub: user.sub };
|
|
765
|
-
} catch (error) {
|
|
766
|
-
if (error instanceof MobileAuthError && (error.code === "oauth" || error.code === "token"))
|
|
767
|
-
return null;
|
|
768
|
-
throw error;
|
|
789
|
+
const guardedFetch = new Proxy(originalFetch, {
|
|
790
|
+
apply: async (target, thisArg, args) => {
|
|
791
|
+
const [input] = args;
|
|
792
|
+
const response = await Reflect.apply(target, thisArg, args);
|
|
793
|
+
if (response.status === HTTP_UNAUTHORIZED && protectedRequest(input)) {
|
|
794
|
+
expire();
|
|
795
|
+
}
|
|
796
|
+
return response;
|
|
769
797
|
}
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
headers: { "content-type": "application/json" },
|
|
778
|
-
method: "POST"
|
|
779
|
-
});
|
|
780
|
-
const body = await responseBody(response);
|
|
781
|
-
if (!response.ok || !isRecord(body) || typeof body.ticket !== "string")
|
|
782
|
-
throw new MobileAuthError(response.ok ? "token" : "oauth", `WebSocket ticket request failed with HTTP ${response.status}.`);
|
|
783
|
-
return body.ticket;
|
|
784
|
-
};
|
|
785
|
-
const revokeRefreshToken = async (refreshToken) => {
|
|
786
|
-
const metadata = await discovery();
|
|
787
|
-
if (metadata.revocation_endpoint)
|
|
788
|
-
await fetchImpl(metadata.revocation_endpoint, {
|
|
789
|
-
body: new URLSearchParams({
|
|
790
|
-
client_id: config.clientId,
|
|
791
|
-
token: refreshToken,
|
|
792
|
-
token_type_hint: "refresh_token"
|
|
793
|
-
}),
|
|
794
|
-
credentials: "omit",
|
|
795
|
-
headers: {
|
|
796
|
-
"content-type": "application/x-www-form-urlencoded"
|
|
797
|
-
},
|
|
798
|
-
method: "POST"
|
|
799
|
-
});
|
|
800
|
-
};
|
|
801
|
-
const signOut = async () => {
|
|
802
|
-
const refreshToken = await config.storage.get(REFRESH_KEY);
|
|
798
|
+
});
|
|
799
|
+
window.fetch = guardedFetch;
|
|
800
|
+
const check = async () => {
|
|
801
|
+
if (checking || expired || disposed)
|
|
802
|
+
return false;
|
|
803
|
+
checking = true;
|
|
804
|
+
lastCheckedAt = Date.now();
|
|
803
805
|
try {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
+
const response = await nativeFetch(statusPath, {
|
|
807
|
+
cache: "no-store",
|
|
808
|
+
credentials: "include",
|
|
809
|
+
headers: { accept: "application/json" }
|
|
810
|
+
});
|
|
811
|
+
if (!response.ok)
|
|
812
|
+
return false;
|
|
813
|
+
const payload = await response.json();
|
|
814
|
+
const sessionExpired = typeof payload === "object" && payload !== null && Reflect.get(payload, "user") === null;
|
|
815
|
+
if (!sessionExpired)
|
|
816
|
+
return false;
|
|
817
|
+
expire();
|
|
818
|
+
return true;
|
|
819
|
+
} catch {
|
|
820
|
+
return false;
|
|
806
821
|
} finally {
|
|
807
|
-
|
|
808
|
-
await Promise.all([
|
|
809
|
-
config.storage.remove(PENDING_KEY),
|
|
810
|
-
config.storage.remove(REFRESH_KEY)
|
|
811
|
-
]);
|
|
822
|
+
checking = false;
|
|
812
823
|
}
|
|
813
824
|
};
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
startPromise = undefined;
|
|
825
|
+
const checkIfDue = () => {
|
|
826
|
+
if (document.visibilityState !== "visible" || Date.now() - lastCheckedAt < checkIntervalMs)
|
|
827
|
+
return;
|
|
828
|
+
check();
|
|
819
829
|
};
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
830
|
+
const checkPersistedSession = (event) => {
|
|
831
|
+
if (!event.persisted)
|
|
832
|
+
return;
|
|
833
|
+
check();
|
|
834
|
+
};
|
|
835
|
+
const dispose = () => {
|
|
836
|
+
if (disposed)
|
|
837
|
+
return;
|
|
838
|
+
disposed = true;
|
|
839
|
+
document.removeEventListener("visibilitychange", checkIfDue);
|
|
840
|
+
window.removeEventListener("focus", checkIfDue);
|
|
841
|
+
window.removeEventListener("pageshow", checkPersistedSession);
|
|
842
|
+
if (window.fetch === guardedFetch)
|
|
843
|
+
window.fetch = originalFetch;
|
|
844
|
+
activeGuard = null;
|
|
831
845
|
};
|
|
846
|
+
document.addEventListener("visibilitychange", checkIfDue);
|
|
847
|
+
window.addEventListener("focus", checkIfDue);
|
|
848
|
+
window.addEventListener("pageshow", checkPersistedSession);
|
|
849
|
+
activeGuard = { check, dispose };
|
|
850
|
+
return activeGuard;
|
|
851
|
+
};
|
|
852
|
+
var isProtectedSessionRequest = ({
|
|
853
|
+
input,
|
|
854
|
+
origin,
|
|
855
|
+
protectedPaths
|
|
856
|
+
}) => {
|
|
857
|
+
const url = requestUrl(input, origin);
|
|
858
|
+
return url?.origin === origin && protectedPaths.some((path) => url.pathname.startsWith(path));
|
|
832
859
|
};
|
|
833
|
-
var createMobileAuthTransport = (client) => ({
|
|
834
|
-
fetch: client.fetch,
|
|
835
|
-
signInEmail: async ({ email }) => {
|
|
836
|
-
await client.signIn({
|
|
837
|
-
authorizationParameters: { login_hint: email }
|
|
838
|
-
});
|
|
839
|
-
return { status: "authenticated" };
|
|
840
|
-
},
|
|
841
|
-
signOut: async () => {
|
|
842
|
-
await client.signOut();
|
|
843
|
-
return null;
|
|
844
|
-
},
|
|
845
|
-
signUpEmail: async ({ email }) => {
|
|
846
|
-
await client.signIn({
|
|
847
|
-
authorizationParameters: {
|
|
848
|
-
login_hint: email,
|
|
849
|
-
screen_hint: "signup"
|
|
850
|
-
}
|
|
851
|
-
});
|
|
852
|
-
return { status: "authenticated" };
|
|
853
|
-
},
|
|
854
|
-
status: async () => {
|
|
855
|
-
const user = await client.status();
|
|
856
|
-
return { user };
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
860
|
// src/redirect.ts
|
|
860
861
|
var isSafeLocalPath = (value) => /^\/(?![/\\])/.test(value);
|
|
861
862
|
var toSafeLocalPath = (value, fallback = "/") => value !== undefined && isSafeLocalPath(value) ? value : fallback;
|
|
@@ -921,5 +922,5 @@ export {
|
|
|
921
922
|
MobileAuthError
|
|
922
923
|
};
|
|
923
924
|
|
|
924
|
-
//# debugId=
|
|
925
|
+
//# debugId=E661DCC8D887DF4364756E2164756E21
|
|
925
926
|
//# sourceMappingURL=index.js.map
|