@jskit-ai/auth-provider-local-core 0.1.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.
@@ -0,0 +1,689 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import {
3
+ AUTH_PASSWORD_MAX_LENGTH,
4
+ AUTH_PASSWORD_MIN_LENGTH
5
+ } from "@jskit-ai/auth-core/shared/authConstraints";
6
+ import { normalizeAuthCapabilities } from "@jskit-ai/auth-core/shared/authCapabilities";
7
+ import { buildSecurityStatusFromAuthMethodsStatus } from "@jskit-ai/auth-core/shared/authSecurityStatus";
8
+ import { normalizeAuthActor, normalizeAuthResult } from "@jskit-ai/auth-core/server/authActor";
9
+ import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
10
+ import { throwUnsupportedAuthOperation } from "@jskit-ai/auth-core/server/unsupportedOperation";
11
+ import { hashPassword, verifyPassword } from "./passwords.js";
12
+ import { randomToken, sha256Base64url, signToken, verifySignedToken } from "./tokens.js";
13
+
14
+ const ACCESS_TOKEN_COOKIE = "jskit_local_access_token";
15
+ const REFRESH_TOKEN_COOKIE = "jskit_local_refresh_token";
16
+ const RECOVERY_TOKEN_COOKIE = "jskit_local_recovery_token";
17
+ const PROVIDER_ID = "local";
18
+ const ACCESS_TTL_SECONDS = 15 * 60;
19
+ const REFRESH_TTL_SECONDS = 60 * 60 * 24 * 30;
20
+ const RECOVERY_SESSION_TTL_SECONDS = 15 * 60;
21
+ const RECOVERY_TOKEN_TTL_SECONDS = 60 * 60;
22
+
23
+ function nowSeconds() {
24
+ return Math.floor(Date.now() / 1000);
25
+ }
26
+
27
+ function isoFromNow(seconds) {
28
+ return new Date(Date.now() + seconds * 1000).toISOString();
29
+ }
30
+
31
+ function isExpiredIso(value) {
32
+ return Date.parse(String(value || "")) <= Date.now();
33
+ }
34
+
35
+ function isNormalSession(session) {
36
+ return (session?.purpose || "normal") === "normal";
37
+ }
38
+
39
+ function normalizeDisplayName(value, email) {
40
+ const displayName = String(value || "").trim();
41
+ if (displayName) {
42
+ return displayName;
43
+ }
44
+ return email.split("@")[0] || "User";
45
+ }
46
+
47
+ function createId(prefix) {
48
+ return `${prefix}_${randomToken(18)}`;
49
+ }
50
+
51
+ function safeRequestCookies(request) {
52
+ return request && request.cookies && typeof request.cookies === "object" ? request.cookies : {};
53
+ }
54
+
55
+ function cookieOptions(isProduction, maxAge) {
56
+ return {
57
+ path: "/",
58
+ httpOnly: true,
59
+ sameSite: "lax",
60
+ secure: isProduction,
61
+ maxAge
62
+ };
63
+ }
64
+
65
+ function clearCookieOptions(isProduction) {
66
+ return [
67
+ { path: "/", httpOnly: true, sameSite: "lax", secure: isProduction, maxAge: 0 },
68
+ { path: "/api", httpOnly: true, sameSite: "lax", secure: isProduction, maxAge: 0 }
69
+ ];
70
+ }
71
+
72
+ function buildProfile(user) {
73
+ return {
74
+ id: user.id,
75
+ authProvider: PROVIDER_ID,
76
+ authProviderUserSid: user.id,
77
+ email: user.email,
78
+ displayName: user.displayName,
79
+ profileSource: "auth-provider"
80
+ };
81
+ }
82
+
83
+ function buildActor(user, profile = null) {
84
+ const profileSource = profile ? "users" : "auth-provider";
85
+ return normalizeAuthActor({
86
+ provider: PROVIDER_ID,
87
+ providerUserId: user.id,
88
+ email: user.email,
89
+ displayName: user.displayName,
90
+ appUserId: profile?.id || null,
91
+ profileSource
92
+ });
93
+ }
94
+
95
+ async function buildAuthPayload({ user, session, profileProjector }) {
96
+ let profile = buildProfile(user);
97
+ if (profileProjector && typeof profileProjector.syncIdentityProfile === "function") {
98
+ profile = await profileProjector.syncIdentityProfile(profile);
99
+ if (!profile || typeof profile !== "object") {
100
+ throw new Error("auth.profile.projector.syncIdentityProfile() must return a profile object.");
101
+ }
102
+ }
103
+ return normalizeAuthResult({
104
+ profile,
105
+ actor: buildActor(user, profileProjector ? profile : null),
106
+ session
107
+ });
108
+ }
109
+
110
+ function buildAccessToken({ user, session, secret, ttlSeconds = ACCESS_TTL_SECONDS }) {
111
+ const issuedAt = nowSeconds();
112
+ return signToken(
113
+ {
114
+ iss: "jskit-auth-local",
115
+ aud: "authenticated",
116
+ sub: user.id,
117
+ sid: session.id,
118
+ purpose: session.purpose || "normal",
119
+ email: user.email,
120
+ iat: issuedAt,
121
+ exp: issuedAt + ttlSeconds,
122
+ jti: randomToken(12)
123
+ },
124
+ secret
125
+ );
126
+ }
127
+
128
+ function buildSessionPayload({ user, session, refreshToken, secret }) {
129
+ const accessToken = buildAccessToken({ user, session, secret });
130
+ return {
131
+ access_token: accessToken,
132
+ refresh_token: refreshToken,
133
+ expires_in: ACCESS_TTL_SECONDS,
134
+ token_type: "bearer",
135
+ purpose: session.purpose || "normal"
136
+ };
137
+ }
138
+
139
+ function validatePasswordInput(password) {
140
+ const normalizedPassword = String(password || "");
141
+ if (
142
+ normalizedPassword.length < AUTH_PASSWORD_MIN_LENGTH ||
143
+ normalizedPassword.length > AUTH_PASSWORD_MAX_LENGTH
144
+ ) {
145
+ throw new AppError(400, "Password must be at least 8 characters.", {
146
+ details: {
147
+ fieldErrors: {
148
+ password: "Password must be at least 8 characters."
149
+ }
150
+ }
151
+ });
152
+ }
153
+ }
154
+
155
+ function validateEmailInput(email) {
156
+ const normalized = normalizeEmail(email);
157
+ if (!normalized || !normalized.includes("@")) {
158
+ throw new AppError(400, "Enter a valid email address.", {
159
+ details: {
160
+ fieldErrors: {
161
+ email: "Enter a valid email address."
162
+ }
163
+ }
164
+ });
165
+ }
166
+ return normalized;
167
+ }
168
+
169
+ async function maybeSendRecoveryEmail(config, recoveryUrl, email) {
170
+ if (!config.smtpConfigured) {
171
+ return;
172
+ }
173
+ const nodemailer = await import("nodemailer");
174
+ const transport = nodemailer.createTransport({
175
+ host: config.smtp.host,
176
+ port: config.smtp.port,
177
+ secure: config.smtp.secure,
178
+ auth: config.smtp.user
179
+ ? {
180
+ user: config.smtp.user,
181
+ pass: config.smtp.password
182
+ }
183
+ : undefined
184
+ });
185
+ await transport.sendMail({
186
+ from: config.smtp.from,
187
+ replyTo: config.smtp.replyTo || undefined,
188
+ to: email,
189
+ subject: "Reset your password",
190
+ text: `Open this link to reset your password:\n\n${recoveryUrl}\n`
191
+ });
192
+ }
193
+
194
+ function createLocalAuthService({ backend, config, profileProjector = null }) {
195
+ if (!backend || typeof backend.withTransaction !== "function") {
196
+ throw new Error("Local auth requires auth.local.backend with withTransaction().");
197
+ }
198
+ if (!config?.sessionSecret) {
199
+ throw new Error("Local auth requires a session secret.");
200
+ }
201
+
202
+ const isProduction = config.nodeEnv === "production";
203
+ const profileProjectionEnabled = typeof profileProjector?.syncIdentityProfile === "function";
204
+ const recoveryDelivery = config.smtpConfigured
205
+ ? "smtp"
206
+ : isProduction
207
+ ? "disabled"
208
+ : config.recoveryDevOutput === "response"
209
+ ? "dev-response"
210
+ : "dev-log";
211
+ const recoveryEnabled = recoveryDelivery !== "disabled";
212
+ const capabilities = normalizeAuthCapabilities({
213
+ provider: {
214
+ id: PROVIDER_ID,
215
+ label: "Local"
216
+ },
217
+ features: {
218
+ password: {
219
+ login: true,
220
+ register: true,
221
+ change: true,
222
+ methodToggle: false
223
+ },
224
+ passwordRecovery: {
225
+ request: recoveryEnabled,
226
+ complete: recoveryEnabled,
227
+ delivery: recoveryDelivery
228
+ },
229
+ otp: {
230
+ login: false
231
+ },
232
+ oauthLogin: {
233
+ enabled: false,
234
+ providers: []
235
+ },
236
+ emailConfirmation: false,
237
+ profileUpdate: true,
238
+ providerLinking: {
239
+ start: false,
240
+ unlink: false
241
+ },
242
+ securityStatus: true,
243
+ signOutOtherSessions: true,
244
+ appProfileProjection: profileProjectionEnabled
245
+ }
246
+ });
247
+
248
+ async function createSessionForUser(tx, user, { purpose = "normal", ttlSeconds = REFRESH_TTL_SECONDS } = {}) {
249
+ const refreshToken = randomToken(36);
250
+ const session = await tx.sessions.create({
251
+ id: createId("ses"),
252
+ userId: user.id,
253
+ tokenHash: sha256Base64url(refreshToken),
254
+ purpose,
255
+ expiresAt: isoFromNow(ttlSeconds)
256
+ });
257
+ return buildSessionPayload({
258
+ user,
259
+ session,
260
+ refreshToken,
261
+ secret: config.sessionSecret
262
+ });
263
+ }
264
+
265
+ async function resolveValidSessionByRefreshToken(refreshToken) {
266
+ const tokenHash = sha256Base64url(refreshToken);
267
+ return backend.withTransaction(async (tx) => {
268
+ const session = await tx.sessions.findByTokenHash(tokenHash);
269
+ if (!session || session.revokedAt || isExpiredIso(session.expiresAt)) {
270
+ return null;
271
+ }
272
+ const user = await tx.users.findById(session.userId);
273
+ if (!user || user.disabled) {
274
+ return null;
275
+ }
276
+ return {
277
+ user,
278
+ session
279
+ };
280
+ });
281
+ }
282
+
283
+ async function register(input = {}) {
284
+ const email = validateEmailInput(input.email);
285
+ validatePasswordInput(input.password);
286
+ const displayName = normalizeDisplayName(input.displayName, email);
287
+ const password = await hashPassword(input.password);
288
+ const result = await backend.withTransaction(async (tx) => {
289
+ const existing = await tx.users.findByEmail(email);
290
+ if (existing) {
291
+ throw new AppError(409, "An account already exists for this email.");
292
+ }
293
+ const user = await tx.users.create({
294
+ id: createId("usr"),
295
+ email,
296
+ displayName,
297
+ password
298
+ });
299
+ return {
300
+ user,
301
+ session: await createSessionForUser(tx, user)
302
+ };
303
+ });
304
+ return {
305
+ ...(await buildAuthPayload({ user: result.user, session: result.session, profileProjector })),
306
+ requiresEmailConfirmation: false
307
+ };
308
+ }
309
+
310
+ async function login(input = {}) {
311
+ const email = validateEmailInput(input.email);
312
+ const password = String(input.password || "");
313
+ return backend.withTransaction(async (tx) => {
314
+ const user = await tx.users.findByEmail(email);
315
+ if (!user || user.disabled || !(await verifyPassword(password, user.password))) {
316
+ throw new AppError(401, "Invalid email or password.");
317
+ }
318
+ const session = await createSessionForUser(tx, user);
319
+ return buildAuthPayload({ user, session, profileProjector });
320
+ });
321
+ }
322
+
323
+ async function authenticateRequest(request) {
324
+ const cookies = safeRequestCookies(request);
325
+ const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "").trim();
326
+ const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "").trim();
327
+
328
+ if (accessToken) {
329
+ const payload = verifySignedToken(accessToken, config.sessionSecret);
330
+ if (payload?.sid && payload?.sub) {
331
+ const resolved = await backend.withTransaction(async (tx) => {
332
+ const session = await tx.sessions.findById(String(payload.sid));
333
+ if (!session || session.revokedAt || isExpiredIso(session.expiresAt)) {
334
+ return null;
335
+ }
336
+ const user = await tx.users.findById(String(payload.sub));
337
+ if (!user || user.disabled) {
338
+ return null;
339
+ }
340
+ return { user, session };
341
+ });
342
+ if (resolved && isNormalSession(resolved.session)) {
343
+ return {
344
+ authenticated: true,
345
+ ...(await buildAuthPayload({ user: resolved.user, session: null, profileProjector })),
346
+ session: null,
347
+ sessionPurpose: resolved.session.purpose || "normal",
348
+ clearSession: false,
349
+ transientFailure: false
350
+ };
351
+ }
352
+ }
353
+ }
354
+
355
+ if (!refreshToken) {
356
+ return {
357
+ authenticated: false,
358
+ clearSession: Boolean(accessToken),
359
+ session: null,
360
+ transientFailure: false
361
+ };
362
+ }
363
+
364
+ const resolved = await resolveValidSessionByRefreshToken(refreshToken);
365
+ if (!resolved) {
366
+ return {
367
+ authenticated: false,
368
+ clearSession: true,
369
+ session: null,
370
+ transientFailure: false
371
+ };
372
+ }
373
+
374
+ if (!isNormalSession(resolved.session)) {
375
+ return {
376
+ authenticated: false,
377
+ clearSession: false,
378
+ session: null,
379
+ transientFailure: false
380
+ };
381
+ }
382
+
383
+ return {
384
+ authenticated: true,
385
+ ...(await buildAuthPayload({ user: resolved.user, session: null, profileProjector })),
386
+ session: buildSessionPayload({
387
+ user: resolved.user,
388
+ session: resolved.session,
389
+ refreshToken,
390
+ secret: config.sessionSecret
391
+ }),
392
+ sessionPurpose: resolved.session.purpose || "normal",
393
+ clearSession: false,
394
+ transientFailure: false
395
+ };
396
+ }
397
+
398
+ function writeSessionCookies(reply, session) {
399
+ if (!reply || !session) {
400
+ return;
401
+ }
402
+ const accessToken = String(session.access_token || "");
403
+ const refreshToken = String(session.refresh_token || "");
404
+ const purpose = String(session.purpose || "normal");
405
+ const isRecoverySession = purpose === "recovery";
406
+ const accessMaxAge = Number(session.expires_in || ACCESS_TTL_SECONDS);
407
+ const refreshMaxAge = isRecoverySession ? accessMaxAge : REFRESH_TTL_SECONDS;
408
+ if (accessToken) {
409
+ reply.setCookie(
410
+ isRecoverySession ? RECOVERY_TOKEN_COOKIE : ACCESS_TOKEN_COOKIE,
411
+ accessToken,
412
+ cookieOptions(isProduction, accessMaxAge)
413
+ );
414
+ }
415
+ if (refreshToken) {
416
+ reply.setCookie(REFRESH_TOKEN_COOKIE, refreshToken, cookieOptions(isProduction, refreshMaxAge));
417
+ }
418
+ }
419
+
420
+ function clearSessionCookies(reply) {
421
+ if (!reply) {
422
+ return;
423
+ }
424
+ for (const options of clearCookieOptions(isProduction)) {
425
+ reply.clearCookie(ACCESS_TOKEN_COOKIE, options);
426
+ reply.clearCookie(REFRESH_TOKEN_COOKIE, options);
427
+ reply.clearCookie(RECOVERY_TOKEN_COOKIE, options);
428
+ }
429
+ }
430
+
431
+ async function logout(request) {
432
+ const cookies = safeRequestCookies(request);
433
+ const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "").trim();
434
+ if (!refreshToken) {
435
+ return {
436
+ ok: true,
437
+ clearSession: true
438
+ };
439
+ }
440
+ await backend.withTransaction(async (tx) => {
441
+ const session = await tx.sessions.findByTokenHash(sha256Base64url(refreshToken));
442
+ if (session) {
443
+ await tx.sessions.revoke(session.id);
444
+ }
445
+ });
446
+ return {
447
+ ok: true,
448
+ clearSession: true
449
+ };
450
+ }
451
+
452
+ async function requestPasswordReset(input = {}) {
453
+ if (!recoveryEnabled) {
454
+ throw new AppError(503, "Password recovery is not configured.");
455
+ }
456
+ const email = validateEmailInput(input.email);
457
+ let recoveryUrl = "";
458
+ await backend.withTransaction(async (tx) => {
459
+ const user = await tx.users.findByEmail(email);
460
+ if (!user || user.disabled) {
461
+ return;
462
+ }
463
+ const token = randomToken(32);
464
+ recoveryUrl = `${config.appPublicUrl.replace(/\/$/, "")}/auth/reset-password?token=${encodeURIComponent(token)}&type=recovery`;
465
+ await tx.recovery.create({
466
+ id: createId("rec"),
467
+ userId: user.id,
468
+ tokenHash: sha256Base64url(token),
469
+ expiresAt: isoFromNow(RECOVERY_TOKEN_TTL_SECONDS)
470
+ });
471
+ });
472
+ if (recoveryUrl) {
473
+ await maybeSendRecoveryEmail(config, recoveryUrl, email);
474
+ if (!config.smtpConfigured && config.recoveryDevOutput === "log" && config.logger?.info) {
475
+ config.logger.info({ recoveryUrl, email }, "Local auth password recovery URL created.");
476
+ }
477
+ }
478
+ return {
479
+ ok: true,
480
+ message: "If an account exists for that email, password reset instructions have been sent.",
481
+ ...(recoveryUrl && !config.smtpConfigured && config.recoveryDevOutput === "response" ? { recoveryUrl } : {})
482
+ };
483
+ }
484
+
485
+ async function completePasswordRecovery(input = {}) {
486
+ if (!recoveryEnabled) {
487
+ throw new AppError(503, "Password recovery is not configured.");
488
+ }
489
+ const token = String(input.code || input.token || "").trim();
490
+ const tokenHash = String(input.tokenHash || "").trim() || (token ? sha256Base64url(token) : "");
491
+ if (!tokenHash) {
492
+ throw new AppError(400, "Recovery token is required.");
493
+ }
494
+ return backend.withTransaction(async (tx) => {
495
+ const recovery = await tx.recovery.findByTokenHash(tokenHash);
496
+ if (!recovery || recovery.usedAt || isExpiredIso(recovery.expiresAt)) {
497
+ throw new AppError(401, "Recovery token is invalid or expired.");
498
+ }
499
+ await tx.recovery.consume(recovery.id);
500
+ const user = await tx.users.findById(recovery.userId);
501
+ if (!user || user.disabled) {
502
+ throw new AppError(401, "Recovery token is invalid or expired.");
503
+ }
504
+ const session = await createSessionForUser(tx, user, {
505
+ purpose: "recovery",
506
+ ttlSeconds: RECOVERY_SESSION_TTL_SECONDS
507
+ });
508
+ return buildAuthPayload({ user, session, profileProjector });
509
+ });
510
+ }
511
+
512
+ async function resetPassword(request, input = {}) {
513
+ validatePasswordInput(input.password);
514
+ const cookies = safeRequestCookies(request);
515
+ const recoveryAccessToken = String(cookies[RECOVERY_TOKEN_COOKIE] || "").trim();
516
+ const payload = verifySignedToken(recoveryAccessToken, config.sessionSecret);
517
+ if (!payload?.sub || !payload?.sid) {
518
+ throw new AppError(401, "Authentication required.");
519
+ }
520
+ const password = await hashPassword(input.password);
521
+ await backend.withTransaction(async (tx) => {
522
+ const session = await tx.sessions.findById(String(payload.sid));
523
+ if (!session || session.revokedAt || isExpiredIso(session.expiresAt)) {
524
+ throw new AppError(401, "Authentication required.");
525
+ }
526
+ if (session.purpose !== "recovery") {
527
+ throw new AppError(401, "Recovery session required.");
528
+ }
529
+ await tx.users.updatePassword(String(payload.sub), password);
530
+ await tx.sessions.revokeForUser(String(payload.sub));
531
+ if (typeof tx.recovery.consumeForUser === "function") {
532
+ await tx.recovery.consumeForUser(String(payload.sub));
533
+ }
534
+ });
535
+ return {
536
+ ok: true,
537
+ message: "Password updated. Sign in with your new password."
538
+ };
539
+ }
540
+
541
+ async function changePassword(request, input = {}) {
542
+ validatePasswordInput(input.newPassword);
543
+ const authResult = await authenticateRequest(request);
544
+ if (authResult.authenticated !== true || authResult.sessionPurpose !== "normal") {
545
+ throw new AppError(401, "Authentication required.");
546
+ }
547
+
548
+ const currentPassword = String(input.currentPassword || "");
549
+ const password = await hashPassword(input.newPassword);
550
+ await backend.withTransaction(async (tx) => {
551
+ const user = await tx.users.findById(authResult.actor.providerUserId);
552
+ if (!user || user.disabled) {
553
+ throw new AppError(401, "Authentication required.");
554
+ }
555
+ if (!(await verifyPassword(currentPassword, user.password))) {
556
+ throw new AppError(401, "Current password is invalid.");
557
+ }
558
+ await tx.users.updatePassword(user.id, password);
559
+ });
560
+
561
+ return {
562
+ ok: true,
563
+ message: "Password updated."
564
+ };
565
+ }
566
+
567
+ async function updateDisplayName(request, input = {}) {
568
+ const authResult = await authenticateRequest(request);
569
+ if (authResult.authenticated !== true) {
570
+ throw new AppError(401, "Authentication required.");
571
+ }
572
+ const displayName = String(input.displayName || "").trim();
573
+ if (!displayName) {
574
+ throw new AppError(400, "Display name is required.");
575
+ }
576
+ return backend.withTransaction(async (tx) => {
577
+ const user = await tx.users.updateProfile(authResult.actor.providerUserId, { displayName });
578
+ if (!user) {
579
+ throw new AppError(401, "Authentication required.");
580
+ }
581
+ return buildAuthPayload({ user, session: null, profileProjector });
582
+ });
583
+ }
584
+
585
+ async function getSecurityStatus(request) {
586
+ const authResult = await authenticateRequest(request);
587
+ if (authResult.authenticated !== true) {
588
+ throw new AppError(401, "Authentication required.");
589
+ }
590
+ return buildSecurityStatusFromAuthMethodsStatus(
591
+ {
592
+ methods: [
593
+ {
594
+ id: "password",
595
+ kind: "password",
596
+ provider: "email",
597
+ label: "Password",
598
+ configured: true,
599
+ enabled: true,
600
+ canDisable: false,
601
+ supportsSecretUpdate: true,
602
+ requiresCurrentPassword: false
603
+ }
604
+ ],
605
+ minimumEnabledMethods: 1,
606
+ enabledMethodsCount: 1
607
+ },
608
+ {
609
+ actions: {
610
+ changePassword: true,
611
+ signOutOtherSessions: true
612
+ }
613
+ }
614
+ );
615
+ }
616
+
617
+ async function signOutOtherSessions(request) {
618
+ const authResult = await authenticateRequest(request);
619
+ if (authResult.authenticated !== true) {
620
+ throw new AppError(401, "Authentication required.");
621
+ }
622
+ const cookies = safeRequestCookies(request);
623
+ const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "").trim();
624
+ const currentSession = refreshToken ? await resolveValidSessionByRefreshToken(refreshToken) : null;
625
+ const count = await backend.withTransaction((tx) =>
626
+ tx.sessions.revokeForUser(authResult.actor.providerUserId, {
627
+ exceptSessionId: currentSession?.session?.id
628
+ })
629
+ );
630
+ return {
631
+ ok: true,
632
+ revokedSessions: count
633
+ };
634
+ }
635
+
636
+ function getCapabilities() {
637
+ return capabilities;
638
+ }
639
+
640
+ return Object.freeze({
641
+ getCapabilities,
642
+ authenticateRequest,
643
+ hasAccessTokenCookie(request) {
644
+ return Boolean(safeRequestCookies(request)[ACCESS_TOKEN_COOKIE] || safeRequestCookies(request)[RECOVERY_TOKEN_COOKIE]);
645
+ },
646
+ hasSessionCookie(request) {
647
+ const cookies = safeRequestCookies(request);
648
+ return Boolean(cookies[ACCESS_TOKEN_COOKIE] || cookies[REFRESH_TOKEN_COOKIE] || cookies[RECOVERY_TOKEN_COOKIE]);
649
+ },
650
+ writeSessionCookies,
651
+ clearSessionCookies,
652
+ register,
653
+ resendRegisterConfirmation() {
654
+ return throwUnsupportedAuthOperation("resendRegisterConfirmation");
655
+ },
656
+ login,
657
+ requestOtpLogin() {
658
+ return throwUnsupportedAuthOperation("requestOtpLogin");
659
+ },
660
+ verifyOtpLogin() {
661
+ return throwUnsupportedAuthOperation("verifyOtpLogin");
662
+ },
663
+ oauthStart() {
664
+ return throwUnsupportedAuthOperation("oauthStart");
665
+ },
666
+ oauthComplete() {
667
+ return throwUnsupportedAuthOperation("oauthComplete");
668
+ },
669
+ requestPasswordReset,
670
+ completePasswordRecovery,
671
+ resetPassword,
672
+ changePassword,
673
+ updateDisplayName,
674
+ getSecurityStatus,
675
+ setPasswordSignInEnabled() {
676
+ return throwUnsupportedAuthOperation("setPasswordSignInEnabled");
677
+ },
678
+ startProviderLink() {
679
+ return throwUnsupportedAuthOperation("startProviderLink");
680
+ },
681
+ unlinkProvider() {
682
+ return throwUnsupportedAuthOperation("unlinkProvider");
683
+ },
684
+ signOutOtherSessions,
685
+ logout
686
+ });
687
+ }
688
+
689
+ export { createLocalAuthService };