or3-provider-basic-auth 0.0.9 → 0.0.10
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/README.md +20 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/BasicAuthChangePasswordModal.client.vue +10 -1
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue +6 -2
- package/dist/runtime/password-policy.d.ts +2 -0
- package/dist/runtime/password-policy.js +4 -0
- package/dist/runtime/server/api/basic-auth/_helpers.js +3 -0
- package/dist/runtime/server/api/basic-auth/change-password.post.js +7 -2
- package/dist/runtime/server/api/basic-auth/register.post.js +7 -2
- package/dist/runtime/server/api/basic-auth/sign-out.post.js +38 -3
- package/dist/runtime/server/lib/cookies.d.ts +2 -0
- package/dist/runtime/server/lib/cookies.js +14 -6
- package/dist/runtime/server/lib/jwt.js +3 -3
- package/dist/runtime/server/lib/password.js +4 -0
- package/dist/runtime/server/lib/session-store.d.ts +8 -0
- package/dist/runtime/server/lib/session-store.js +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ Optional:
|
|
|
51
51
|
- `OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD`
|
|
52
52
|
- `OR3_BASIC_AUTH_RATE_LIMIT_BACKEND` (`sqlite` default; `memory` is per-process and intended for single-instance development only)
|
|
53
53
|
- `OR3_BASIC_AUTH_ALLOW_INSECURE_DEV` (`true` in non-production to start with missing secrets instead of failing)
|
|
54
|
+
- `OR3_PLUGIN_DEV_COOKIE_SCOPE` (set by the isolated plugin development launcher to keep browser sessions separate across local hosts)
|
|
54
55
|
|
|
55
56
|
Registration mode is a core-auth policy, not a provider setting:
|
|
56
57
|
|
|
@@ -72,6 +73,7 @@ Strict-mode behavior:
|
|
|
72
73
|
- Access JWTs are short-lived and validated by `basicAuthProvider.getSession(event)`.
|
|
73
74
|
- Refresh tokens are rotated and hashed at rest; replay attempts revoke active sessions.
|
|
74
75
|
- The refresh cookie is scoped to `/api` so `/api/auth/session` can transparently rotate expired access tokens during session resolution.
|
|
76
|
+
- Sign-out never refreshes the session: the user is resolved without rotation, and revoking a token also revokes the sessions it was rotated into and clears their rotation grace, so a replayed predecessor cannot recover a successor after logout.
|
|
75
77
|
- The SQLite DB file and directory are hardened to mode `0600`/`0700`; the DB uses WAL journaling.
|
|
76
78
|
|
|
77
79
|
## Runtime entrypoints
|
|
@@ -90,6 +92,8 @@ Strict-mode behavior:
|
|
|
90
92
|
| `src/runtime/plugins/auth-status.client.ts` | Client auth-status resolver + silent-refresh recovery |
|
|
91
93
|
| `src/runtime/components/*.client.vue` | Provider-owned auth UI (modals, user menu, sidebar button, lock page) |
|
|
92
94
|
|
|
95
|
+
New passwords must be at least eight characters and no more than 72 UTF-8 bytes, matching bcrypt's input limit. Existing account hashes remain verifiable, including passwords created before this limit was enforced.
|
|
96
|
+
|
|
93
97
|
The registered sidebar auth adapter accepts `layout="rail"` (default) and
|
|
94
98
|
`layout="more-sheet"`. In the More sheet it renders Account/Login with the
|
|
95
99
|
host's shared full-width `.more-row` structure, while keeping the existing
|
|
@@ -134,3 +138,19 @@ bun run type-check
|
|
|
134
138
|
bun run test
|
|
135
139
|
bun run build
|
|
136
140
|
```
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
### Testing local changes in OR3 Chat
|
|
144
|
+
|
|
145
|
+
With this repository beside `or3-chat`, run `bun install` here once, then
|
|
146
|
+
`bun run dev:ssr` from Chat. Chat's dev wrapper rebuilds the local provider and
|
|
147
|
+
prints its selected path; restart it after provider edits. Missing repositories
|
|
148
|
+
or failed builds fall back to installed packages with a warning.
|
|
149
|
+
`OR3_LOCAL_PROVIDERS=false` disables local selection. Production builds use the
|
|
150
|
+
installed package, so local development does not publish these changes.
|
|
151
|
+
|
|
152
|
+
Basic Auth's database always uses `better-sqlite3`, independently of the sync
|
|
153
|
+
provider's SQLite driver. If the source launcher selects Bun for sync, this
|
|
154
|
+
native binding must also work under the project-pinned Bun version. The launcher
|
|
155
|
+
checks it in that runtime before starting Nuxt and reports incompatible bindings
|
|
156
|
+
or runtime crashes. Managed Cloud uses Node with `better-sqlite3`.
|
package/dist/module.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
<USeparator />
|
|
33
33
|
|
|
34
|
-
<UFormField label="New Password" name="newPassword">
|
|
34
|
+
<UFormField label="New Password" name="newPassword" hint="At least 8 characters; at most 72 UTF-8 bytes">
|
|
35
35
|
<UInput
|
|
36
36
|
v-model="state.newPassword"
|
|
37
37
|
type="password"
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
|
|
84
84
|
<script setup>
|
|
85
85
|
import { computed, reactive, ref } from "vue";
|
|
86
|
+
import { newPasswordFitsBcrypt } from "../password-policy";
|
|
86
87
|
const modalUi = {
|
|
87
88
|
overlay: "basic-auth-modal-overlay",
|
|
88
89
|
content: "basic-auth-modal-content sm:max-w-[400px]"
|
|
@@ -108,6 +109,14 @@ function close() {
|
|
|
108
109
|
emit("update:modelValue", false);
|
|
109
110
|
}
|
|
110
111
|
async function onSubmit() {
|
|
112
|
+
if (state.newPassword.length < 8) {
|
|
113
|
+
errorMessage.value = "New password must be at least 8 characters.";
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!newPasswordFitsBcrypt(state.newPassword)) {
|
|
117
|
+
errorMessage.value = "New password must be 72 UTF-8 bytes or fewer.";
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
111
120
|
if (state.newPassword !== state.confirmNewPassword) {
|
|
112
121
|
errorMessage.value = "Passwords do not match";
|
|
113
122
|
return;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<UFormField
|
|
47
47
|
label="Password"
|
|
48
48
|
name="password"
|
|
49
|
-
hint="At least 8 characters"
|
|
49
|
+
hint="At least 8 characters; at most 72 UTF-8 bytes"
|
|
50
50
|
>
|
|
51
51
|
<UInput
|
|
52
52
|
v-model="state.password"
|
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
|
|
113
113
|
<script setup>
|
|
114
114
|
import { computed, reactive, ref } from "vue";
|
|
115
|
+
import { newPasswordFitsBcrypt } from "../password-policy";
|
|
115
116
|
const modalUi = {
|
|
116
117
|
overlay: "basic-auth-modal-overlay",
|
|
117
118
|
content: "basic-auth-modal-content sm:max-w-[420px]"
|
|
@@ -150,6 +151,9 @@ function validateClientSide() {
|
|
|
150
151
|
if (state.password.length > 512) {
|
|
151
152
|
return "Password is too long.";
|
|
152
153
|
}
|
|
154
|
+
if (!newPasswordFitsBcrypt(state.password)) {
|
|
155
|
+
return "Password must be 72 UTF-8 bytes or fewer.";
|
|
156
|
+
}
|
|
153
157
|
if (state.password !== state.confirmPassword) {
|
|
154
158
|
return "Passwords must match.";
|
|
155
159
|
}
|
|
@@ -186,7 +190,7 @@ async function onSubmit() {
|
|
|
186
190
|
const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
|
187
191
|
const serverMessage = readFetchErrorMessage(error);
|
|
188
192
|
if (statusCode === 400) {
|
|
189
|
-
errorMessage.value = serverMessage || "Please check your email and password (
|
|
193
|
+
errorMessage.value = serverMessage || "Please check your email and password (8 characters minimum; 72 UTF-8 bytes maximum).";
|
|
190
194
|
} else if (statusCode === 403) {
|
|
191
195
|
errorMessage.value = serverMessage || "Registration is restricted for this instance.";
|
|
192
196
|
} else if (statusCode === 401) {
|
|
@@ -18,6 +18,9 @@ export async function parseBodyWithSchema(event, schema) {
|
|
|
18
18
|
const firstIssue = result.error.issues[0];
|
|
19
19
|
const path = firstIssue?.path?.join(".") || "input";
|
|
20
20
|
const detail = firstIssue?.message || "Invalid request";
|
|
21
|
+
if (detail === "Password must be 72 UTF-8 bytes or fewer.") {
|
|
22
|
+
throw createError({ statusCode: 400, statusMessage: detail });
|
|
23
|
+
}
|
|
21
24
|
if (path === "password" || path === "confirmPassword") {
|
|
22
25
|
throw createError({
|
|
23
26
|
statusCode: 400,
|
|
@@ -11,11 +11,16 @@ import {
|
|
|
11
11
|
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
12
12
|
import { basicAuthProvider } from "../../auth/basic-auth-provider.js";
|
|
13
13
|
import { assertBasicAuthReady, noStore, parseBodyWithSchema } from "./_helpers.js";
|
|
14
|
+
import { newPasswordFitsBcrypt } from "../../../password-policy.js";
|
|
14
15
|
const changePasswordSchema = z.object({
|
|
15
16
|
// Existing accounts may have legacy short passwords; only new password is length-enforced.
|
|
16
17
|
currentPassword: z.string().min(1).max(512),
|
|
17
|
-
newPassword: z.string().min(8).max(512),
|
|
18
|
-
|
|
18
|
+
newPassword: z.string().min(8).max(512).refine(newPasswordFitsBcrypt, {
|
|
19
|
+
message: "Password must be 72 UTF-8 bytes or fewer."
|
|
20
|
+
}),
|
|
21
|
+
confirmNewPassword: z.string().min(8).max(512).refine(newPasswordFitsBcrypt, {
|
|
22
|
+
message: "Password must be 72 UTF-8 bytes or fewer."
|
|
23
|
+
})
|
|
19
24
|
}).refine((input) => input.newPassword === input.confirmNewPassword, {
|
|
20
25
|
message: "Passwords must match"
|
|
21
26
|
});
|
|
@@ -23,11 +23,16 @@ import {
|
|
|
23
23
|
getSessionMetadataFromEvent
|
|
24
24
|
} from "../../lib/session-store.js";
|
|
25
25
|
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
26
|
+
import { newPasswordFitsBcrypt } from "../../../password-policy.js";
|
|
26
27
|
import { assertBasicAuthReady, noStore, parseBodyWithSchema } from "./_helpers.js";
|
|
27
28
|
const registerSchema = z.object({
|
|
28
29
|
email: z.string().email().max(320),
|
|
29
|
-
password: z.string().min(8).max(512),
|
|
30
|
-
|
|
30
|
+
password: z.string().min(8).max(512).refine(newPasswordFitsBcrypt, {
|
|
31
|
+
message: "Password must be 72 UTF-8 bytes or fewer."
|
|
32
|
+
}),
|
|
33
|
+
confirmPassword: z.string().min(8).max(512).refine(newPasswordFitsBcrypt, {
|
|
34
|
+
message: "Password must be 72 UTF-8 bytes or fewer."
|
|
35
|
+
}),
|
|
31
36
|
displayName: z.string().trim().min(1).max(120).optional(),
|
|
32
37
|
inviteToken: z.string().trim().min(1).max(4096).optional()
|
|
33
38
|
}).refine((value) => value.password === value.confirmPassword, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineEventHandler } from "h3";
|
|
2
|
+
import { revokeHostActivationsForUser } from "~~/server/utils/plugins/isolation/activation-registry";
|
|
2
3
|
import { clearAuthCookies } from "../../lib/cookies.js";
|
|
3
4
|
import {
|
|
4
5
|
getAccessTokenFromEvent,
|
|
@@ -7,26 +8,60 @@ import {
|
|
|
7
8
|
verifyRefreshToken
|
|
8
9
|
} from "../../lib/jwt.js";
|
|
9
10
|
import { enforceBasicAuthRateLimit } from "../../lib/rate-limit.js";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
findAccountById,
|
|
13
|
+
findUsableSessionInChain,
|
|
14
|
+
revokeSessionChain
|
|
15
|
+
} from "../../lib/session-store.js";
|
|
11
16
|
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
12
17
|
import { assertBasicAuthReady, noStore } from "./_helpers.js";
|
|
18
|
+
async function resolveSignOutUserId(event) {
|
|
19
|
+
const accessToken = getAccessTokenFromEvent(event);
|
|
20
|
+
if (accessToken) {
|
|
21
|
+
const claims = await verifyAccessToken(accessToken);
|
|
22
|
+
if (claims && findUsableSessionInChain(claims.sid)) {
|
|
23
|
+
const account = findAccountById(claims.sub);
|
|
24
|
+
if (account && account.token_version === claims.ver) {
|
|
25
|
+
return account.id;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const refreshToken = getRefreshTokenFromEvent(event);
|
|
30
|
+
if (refreshToken) {
|
|
31
|
+
const claims = await verifyRefreshToken(refreshToken);
|
|
32
|
+
if (claims && findUsableSessionInChain(claims.sid)) {
|
|
33
|
+
const account = findAccountById(claims.sub);
|
|
34
|
+
if (account && account.token_version === claims.ver) {
|
|
35
|
+
return account.id;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
13
41
|
export async function handleSignOut(event) {
|
|
14
42
|
assertBasicAuthReady(event);
|
|
15
43
|
noStore(event);
|
|
16
44
|
enforceMutationOriginPolicy(event);
|
|
17
45
|
enforceBasicAuthRateLimit(event, "basic-auth:sign-out");
|
|
46
|
+
try {
|
|
47
|
+
const userId = await resolveSignOutUserId(event);
|
|
48
|
+
if (userId) {
|
|
49
|
+
revokeHostActivationsForUser(userId, "logout");
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
18
53
|
const refreshToken = getRefreshTokenFromEvent(event);
|
|
19
54
|
if (refreshToken) {
|
|
20
55
|
const refreshClaims = await verifyRefreshToken(refreshToken);
|
|
21
56
|
if (refreshClaims) {
|
|
22
|
-
|
|
57
|
+
revokeSessionChain(refreshClaims.sid);
|
|
23
58
|
}
|
|
24
59
|
}
|
|
25
60
|
const accessToken = getAccessTokenFromEvent(event);
|
|
26
61
|
if (accessToken) {
|
|
27
62
|
const accessClaims = await verifyAccessToken(accessToken);
|
|
28
63
|
if (accessClaims) {
|
|
29
|
-
|
|
64
|
+
revokeSessionChain(accessClaims.sid);
|
|
30
65
|
}
|
|
31
66
|
}
|
|
32
67
|
clearAuthCookies(event);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type H3Event } from 'h3';
|
|
2
|
+
export declare function accessCookieName(): string;
|
|
3
|
+
export declare function refreshCookieName(): string;
|
|
2
4
|
export declare function setAccessCookie(event: H3Event, token: string, maxAgeSeconds: number): void;
|
|
3
5
|
export declare function setRefreshCookie(event: H3Event, token: string, maxAgeSeconds: number): void;
|
|
4
6
|
export declare function clearAuthCookies(event: H3Event): void;
|
|
@@ -9,6 +9,14 @@ import {
|
|
|
9
9
|
function isProduction() {
|
|
10
10
|
return process.env.NODE_ENV === "production";
|
|
11
11
|
}
|
|
12
|
+
export function accessCookieName() {
|
|
13
|
+
const scope = process.env.OR3_PLUGIN_DEV_COOKIE_SCOPE;
|
|
14
|
+
return scope ? `${ACCESS_COOKIE_NAME}_${scope}` : ACCESS_COOKIE_NAME;
|
|
15
|
+
}
|
|
16
|
+
export function refreshCookieName() {
|
|
17
|
+
const scope = process.env.OR3_PLUGIN_DEV_COOKIE_SCOPE;
|
|
18
|
+
return scope ? `${REFRESH_COOKIE_NAME}_${scope}` : REFRESH_COOKIE_NAME;
|
|
19
|
+
}
|
|
12
20
|
function refreshCookieBase() {
|
|
13
21
|
return {
|
|
14
22
|
httpOnly: true,
|
|
@@ -17,7 +25,7 @@ function refreshCookieBase() {
|
|
|
17
25
|
};
|
|
18
26
|
}
|
|
19
27
|
export function setAccessCookie(event, token, maxAgeSeconds) {
|
|
20
|
-
setCookie(event,
|
|
28
|
+
setCookie(event, accessCookieName(), token, {
|
|
21
29
|
httpOnly: true,
|
|
22
30
|
sameSite: "lax",
|
|
23
31
|
secure: isProduction(),
|
|
@@ -26,28 +34,28 @@ export function setAccessCookie(event, token, maxAgeSeconds) {
|
|
|
26
34
|
});
|
|
27
35
|
}
|
|
28
36
|
export function setRefreshCookie(event, token, maxAgeSeconds) {
|
|
29
|
-
deleteCookie(event,
|
|
37
|
+
deleteCookie(event, refreshCookieName(), {
|
|
30
38
|
...refreshCookieBase(),
|
|
31
39
|
path: LEGACY_REFRESH_COOKIE_PATH
|
|
32
40
|
});
|
|
33
|
-
setCookie(event,
|
|
41
|
+
setCookie(event, refreshCookieName(), token, {
|
|
34
42
|
...refreshCookieBase(),
|
|
35
43
|
path: REFRESH_COOKIE_PATH,
|
|
36
44
|
maxAge: maxAgeSeconds
|
|
37
45
|
});
|
|
38
46
|
}
|
|
39
47
|
export function clearAuthCookies(event) {
|
|
40
|
-
deleteCookie(event,
|
|
48
|
+
deleteCookie(event, accessCookieName(), {
|
|
41
49
|
httpOnly: true,
|
|
42
50
|
sameSite: "lax",
|
|
43
51
|
secure: isProduction(),
|
|
44
52
|
path: ACCESS_COOKIE_PATH
|
|
45
53
|
});
|
|
46
|
-
deleteCookie(event,
|
|
54
|
+
deleteCookie(event, refreshCookieName(), {
|
|
47
55
|
...refreshCookieBase(),
|
|
48
56
|
path: REFRESH_COOKIE_PATH
|
|
49
57
|
});
|
|
50
|
-
deleteCookie(event,
|
|
58
|
+
deleteCookie(event, refreshCookieName(), {
|
|
51
59
|
...refreshCookieBase(),
|
|
52
60
|
path: LEGACY_REFRESH_COOKIE_PATH
|
|
53
61
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "node:crypto";
|
|
2
2
|
import { getCookie } from "h3";
|
|
3
3
|
import jwt from "jsonwebtoken";
|
|
4
|
-
import {
|
|
4
|
+
import { accessCookieName, refreshCookieName } from "./cookies.js";
|
|
5
5
|
import { getBasicAuthConfig } from "./config.js";
|
|
6
6
|
export async function signAccessToken(input) {
|
|
7
7
|
const config = getBasicAuthConfig();
|
|
@@ -67,10 +67,10 @@ export async function verifyRefreshToken(token) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
export function getAccessTokenFromEvent(event) {
|
|
70
|
-
return getCookie(event,
|
|
70
|
+
return getCookie(event, accessCookieName()) ?? null;
|
|
71
71
|
}
|
|
72
72
|
export function getRefreshTokenFromEvent(event) {
|
|
73
|
-
return getCookie(event,
|
|
73
|
+
return getCookie(event, refreshCookieName()) ?? null;
|
|
74
74
|
}
|
|
75
75
|
export function hashRefreshToken(token) {
|
|
76
76
|
return createHash("sha256").update(token).digest("hex");
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import bcrypt from "bcryptjs";
|
|
2
|
+
import { newPasswordFitsBcrypt } from "../../password-policy.js";
|
|
2
3
|
const PASSWORD_COST = 12;
|
|
3
4
|
export async function hashPassword(password) {
|
|
5
|
+
if (!newPasswordFitsBcrypt(password)) {
|
|
6
|
+
throw new RangeError("New passwords must be 72 UTF-8 bytes or fewer.");
|
|
7
|
+
}
|
|
4
8
|
return await bcrypt.hash(password, PASSWORD_COST);
|
|
5
9
|
}
|
|
6
10
|
export async function verifyPassword(password, hash) {
|
|
@@ -61,6 +61,14 @@ export declare function ensureBootstrapAccount(input: CreateAccountInput): Basic
|
|
|
61
61
|
export declare function createAuthSession(input: CreateSessionInput): BasicAuthSession;
|
|
62
62
|
export declare function findSessionById(sessionId: string): BasicAuthSession | null;
|
|
63
63
|
export declare function revokeSessionById(sessionId: string, revokedAtMs?: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Revoke a session and every successor it was rotated into, clearing rotation
|
|
66
|
+
* grace on each. A replayed rotated refresh token therefore cannot recover its
|
|
67
|
+
* successor after sign-out.
|
|
68
|
+
*/
|
|
69
|
+
export declare function revokeSessionChain(sessionId: string, revokedAtMs?: number): void;
|
|
70
|
+
/** First usable session walking from `sessionId` through rotation successors. */
|
|
71
|
+
export declare function findUsableSessionInChain(sessionId: string): BasicAuthSession | null;
|
|
64
72
|
export declare function revokeAllSessionsForAccount(accountId: string, revokedAtMs?: number): void;
|
|
65
73
|
export declare function rotateSession(input: RotationInput): RotationResult;
|
|
66
74
|
export declare function updatePasswordAndRevokeSessions(accountId: string, newPasswordHash: string): void;
|
|
@@ -140,6 +140,36 @@ export function revokeSessionById(sessionId, revokedAtMs = Date.now()) {
|
|
|
140
140
|
"UPDATE basic_auth_sessions SET revoked_at = COALESCE(revoked_at, ?) WHERE id = ?"
|
|
141
141
|
).run(revokedAtMs, sessionId);
|
|
142
142
|
}
|
|
143
|
+
export function revokeSessionChain(sessionId, revokedAtMs = Date.now()) {
|
|
144
|
+
const db = getBasicAuthDb();
|
|
145
|
+
db.transaction(() => {
|
|
146
|
+
const visited = /* @__PURE__ */ new Set();
|
|
147
|
+
let currentId = sessionId;
|
|
148
|
+
while (currentId && !visited.has(currentId)) {
|
|
149
|
+
visited.add(currentId);
|
|
150
|
+
const session = findSessionById(currentId);
|
|
151
|
+
if (!session) break;
|
|
152
|
+
db.prepare(
|
|
153
|
+
`UPDATE basic_auth_sessions
|
|
154
|
+
SET revoked_at = COALESCE(revoked_at, ?),
|
|
155
|
+
rotation_grace_until = NULL,
|
|
156
|
+
rotation_grace_refresh_token = NULL
|
|
157
|
+
WHERE id = ?`
|
|
158
|
+
).run(revokedAtMs, currentId);
|
|
159
|
+
currentId = session.replaced_by_session_id;
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
}
|
|
163
|
+
export function findUsableSessionInChain(sessionId) {
|
|
164
|
+
const visited = /* @__PURE__ */ new Set();
|
|
165
|
+
let current = findSessionById(sessionId);
|
|
166
|
+
while (current && !visited.has(current.id)) {
|
|
167
|
+
if (isSessionUsable(current)) return current;
|
|
168
|
+
visited.add(current.id);
|
|
169
|
+
current = current.replaced_by_session_id ? findSessionById(current.replaced_by_session_id) : null;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
143
173
|
export function revokeAllSessionsForAccount(accountId, revokedAtMs = Date.now()) {
|
|
144
174
|
const db = getBasicAuthDb();
|
|
145
175
|
db.prepare(
|