Merge branch 'Huskydog9988-db-store' into develop

This commit is contained in:
DecDuck
2025-04-04 10:37:08 +11:00
20 changed files with 436 additions and 125 deletions

View File

@@ -39,7 +39,7 @@ export default defineNuxtConfig({
},
scheduledTasks: {
"0 * * * *": ["cleanup:invitations"],
"0 * * * *": ["cleanup:invitations", "cleanup:sessions"],
},
compressPublicAssets: true,

View File

@@ -28,6 +28,7 @@
"fast-fuzzy": "^1.12.0",
"file-type-mime": "^0.4.3",
"jdenticon": "^3.3.0",
"lru-cache": "^11.1.0",
"micromark": "^4.0.1",
"moment": "^2.30.1",
"nuxt": "3.15.4",
@@ -56,7 +57,9 @@
"h3": "^1.13.0",
"postcss": "^8.4.47",
"sass": "^1.79.4",
"tailwindcss": "^4.0.0"
"tailwindcss": "^4.0.0",
"typescript": "^5.8.2",
"vue-tsc": "^2.2.8"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"overrides": {

View File

@@ -214,7 +214,7 @@ const validEmail = computed(
() => !(emailValidator(email.value) instanceof type.errors)
);
const usernameValidator = type("string.lower.preformatted >= 5");
const usernameValidator = type("string.alphanumeric >= 5").to("string.lower");
const validUsername = computed(
() => !(usernameValidator(username.value) instanceof type.errors)
);

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Certificate" (
"id" TEXT NOT NULL,
"privateKey" TEXT NOT NULL,
"certificate" TEXT NOT NULL,
"blacklisted" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "Certificate_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"token" TEXT NOT NULL,
"data" JSONB NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("token")
);

View File

@@ -0,0 +1,13 @@
/*
Warnings:
- Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty.
- Added the required column `userId` to the `Session` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Session" ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL,
ADD COLUMN "userId" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -42,3 +42,22 @@ model APIToken {
@@index([token])
}
model Certificate {
id String @id @default(uuid())
privateKey String
certificate String
blacklisted Boolean @default(false)
}
model Session {
token String @id
expiresAt DateTime
userId String
user User? @relation(fields: [userId], references: [id])
data Json // misc extra data
}

View File

@@ -14,7 +14,8 @@ model User {
collections Collection[]
articles Article[]
tokens APIToken[]
tokens APIToken[]
sessions Session[]
saves SaveSlot[]
}

View File

@@ -8,24 +8,30 @@ import {
} from "~/server/internal/security/simple";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => {
const body = await readBody(h3);
const signinValidator = type({
username: "string",
password: "string",
"rememberMe?": "boolean | undefined",
});
export default defineEventHandler(async (h3) => {
const body = signinValidator(await readBody(h3));
if (body instanceof type.errors) {
// hover out.summary to see validation errors
console.error(body.summary);
const username = body.username;
const password = body.password;
const rememberMe = body.rememberMe ?? false;
if (username === undefined || password === undefined)
throw createError({
statusCode: 403,
statusMessage: "Username or password missing from request.",
statusCode: 400,
statusMessage: body.summary,
});
}
const authMek = await prisma.linkedAuthMec.findFirst({
where: {
mec: AuthMec.Simple,
enabled: true,
user: {
username,
username: body.username,
},
},
include: {
@@ -62,14 +68,14 @@ export default defineEventHandler(async (h3) => {
"Invalid password state. Please contact the server administrator.",
});
if (!(await checkHashBcrypt(password, hash)))
if (!(await checkHashBcrypt(body.password, hash)))
throw createError({
statusCode: 401,
statusMessage: "Invalid username or password.",
});
// TODO: send user to forgot password screen or something to force them to change their password to new system
await sessionHandler.setUserId(h3, authMek.userId, rememberMe);
await sessionHandler.signin(h3, authMek.userId, body.rememberMe);
return { result: true, userId: authMek.userId };
}
@@ -82,13 +88,12 @@ export default defineEventHandler(async (h3) => {
"Invalid password state. Please contact the server administrator.",
});
if (!(await checkHashArgon2(password, hash)))
if (!(await checkHashArgon2(body.password, hash)))
throw createError({
statusCode: 401,
statusMessage: "Invalid username or password.",
});
await sessionHandler.setUserId(h3, authMek.userId, rememberMe);
await sessionHandler.signin(h3, authMek.userId, body.rememberMe);
return { result: true, userId: authMek.userId };
});

View File

@@ -2,8 +2,8 @@ import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => {
const userId = await sessionHandler.getUserId(h3);
if (!userId) throw createError({ statusCode: 403 });
const user = await sessionHandler.getSession(h3);
if (!user) throw createError({ statusCode: 403 });
const query = getQuery(h3);
const providedClientId = query.id?.toString();
@@ -13,16 +13,14 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Provide client ID in request params as 'id'",
});
const data = await clientHandler.fetchClientMetadata(
providedClientId
);
const data = await clientHandler.fetchClientMetadata(providedClientId);
if (!data)
throw createError({
statusCode: 404,
statusMessage: "Request not found.",
});
await clientHandler.attachUserId(providedClientId, userId);
await clientHandler.attachUserId(providedClientId, user.userId);
return data;
});

View File

@@ -2,8 +2,8 @@ import clientHandler from "~/server/internal/clients/handler";
import sessionHandler from "~/server/internal/session";
export default defineEventHandler(async (h3) => {
const userId = await sessionHandler.getUserId(h3);
if (!userId) throw createError({ statusCode: 403 });
const user = await sessionHandler.getSession(h3);
if (!user) throw createError({ statusCode: 403 });
const body = await readBody(h3);
const clientId = await body.id;

View File

@@ -81,8 +81,8 @@ class ACLManager {
if (!request)
throw new Error("Native web requests not available - weird deployment?");
// Sessions automatically have all ACLs
const userId = await sessionHandler.getUserId(request);
if (userId) return userId;
const user = await sessionHandler.getSession(request);
if (user) return user.userId;
const authorizationToken = this.getAuthorizationToken(request);
if (!authorizationToken) return undefined;
@@ -116,9 +116,11 @@ class ACLManager {
) {
if (!request)
throw new Error("Native web requests not available - weird deployment?");
const userId = await sessionHandler.getUserId(request);
if (userId) {
const user = await prisma.user.findUnique({ where: { id: userId } });
const userSession = await sessionHandler.getSession(request);
if (userSession) {
const user = await prisma.user.findUnique({
where: { id: userSession.userId },
});
if (!user) return false;
if (user.admin) return true;
return false;

View File

@@ -1,6 +1,7 @@
import path from "path";
import fs from "fs";
import { CertificateBundle } from "./ca";
import prisma from "../db/database";
export type CertificateStore = {
store(name: string, data: CertificateBundle): Promise<void>;
@@ -33,3 +34,63 @@ export const fsCertificateStore = (base: string) => {
};
return store;
};
export const dbCertificateStore = () => {
const store: CertificateStore = {
async store(name: string, data: CertificateBundle) {
await prisma.certificate.upsert({
where: {
id: name,
},
create: {
id: name,
privateKey: data.priv,
certificate: data.cert,
},
update: {
privateKey: data.priv,
certificate: data.cert,
},
});
},
async fetch(name: string) {
const result = await prisma.certificate.findUnique({
where: {
id: name,
},
select: {
privateKey: true,
certificate: true,
},
});
if (result === null) return undefined;
return {
priv: result.privateKey,
cert: result.certificate,
};
},
async blacklistCertificate(name: string) {
await prisma.certificate.update({
where: {
id: name,
},
data: {
blacklisted: true,
},
});
},
async checkBlacklistCertificate(name: string): Promise<boolean> {
const result = await prisma.certificate.findUnique({
where: {
id: name,
},
select: {
blacklisted: true,
},
});
if (result === null) return false;
return result.blacklisted;
},
};
return store;
};

View File

@@ -0,0 +1,69 @@
import { LRUCache } from "lru-cache";
import prisma from "../db/database";
import { Session, SessionProvider } from "./types";
import { Prisma } from "@prisma/client";
export default function createDBSessionHandler(): SessionProvider {
const cache = new LRUCache<string, Session>({
max: 50, // number of items
ttl: 30 * 100, // 30s (in ms)
});
return {
async setSession(token, session) {
cache.set(token, session);
// const strData = JSON.stringify(data);
await prisma.session.upsert({
where: {
token,
},
create: {
token,
...session,
},
update: session,
});
return true;
},
async updateSession(token, data) {
return await this.setSession(token, data);
},
async getSession<T extends Session>(token: string) {
const cached = cache.get(token);
if (cached !== undefined) return cached as T;
const result = await prisma.session.findUnique({
where: {
token,
},
});
if (result === null) return undefined;
// i hate casting
// need to cast to unknown since result.data can be an N deep json object technically
// ts doesn't like that be cast down to the more constraining session type
return result as unknown as T;
},
async removeSession(token) {
cache.delete(token);
await prisma.session.delete({
where: {
token,
},
});
return true;
},
async cleanupSessions() {
const now = new Date();
await prisma.session.deleteMany({
where: {
expiresAt: {
lt: now,
},
},
});
},
};
}

View File

@@ -1,11 +1,11 @@
import { H3Event, Session } from "h3";
import { H3Event } from "h3";
import createMemorySessionProvider from "./memory";
import { SessionProvider } from "./types";
import prisma from "../db/database";
import { v4 as uuidv4 } from "uuid";
import { Session, SessionProvider } from "./types";
import { randomUUID } from "node:crypto";
import moment from "moment";
import { parse as parseCookies } from "cookie-es";
import { MinimumRequestObject } from "~/server/h3";
import createDBSessionHandler from "./db";
/*
This implementation may need work.
@@ -13,9 +13,7 @@ This implementation may need work.
It exposes an API that should stay static, but there are plenty of opportunities for optimisation/organisation under the hood
*/
const userSessionKey = "_userSession";
const userIdKey = "_userId";
const dropTokenCookie = "drop-token";
const dropTokenCookieName = "drop-token";
const normalSessionLength = [31, "days"];
const extendedSessionLength = [1, "year"];
@@ -24,88 +22,97 @@ export class SessionHandler {
constructor() {
// Create a new provider
this.sessionProvider = createMemorySessionProvider();
this.sessionProvider = createDBSessionHandler();
// this.sessionProvider = createMemorySessionProvider();
}
private getSessionToken(request: MinimumRequestObject | undefined) {
if(!request) throw new Error("Native web request not available");
const cookieHeader = request.headers.get("Cookie");
if (!cookieHeader) return undefined;
const cookies = parseCookies(cookieHeader);
const cookie = cookies[dropTokenCookie];
return cookie;
}
private async createSession(h3: H3Event, extend = false) {
const token = uuidv4();
const expiry = moment().add(
...(extend ? extendedSessionLength : normalSessionLength)
);
setCookie(h3, dropTokenCookie, token, { expires: expiry.toDate() });
this.sessionProvider.setSession(dropTokenCookie, {});
return token;
}
getDropTokenCookie() {
return dropTokenCookie;
async signin(h3: H3Event, userId: string, rememberMe: boolean = false) {
const expiresAt = this.createExipreAt(rememberMe);
const token = this.createSessionCookie(h3, expiresAt);
return await this.sessionProvider.setSession(token, {
userId,
expiresAt,
data: {},
});
}
/**
* Get a session associated with a request
* @returns session
*/
async getSession<T extends Session>(request: MinimumRequestObject) {
const token = this.getSessionToken(request);
if (!token) return undefined;
const data = await this.sessionProvider.getSession<{ [userSessionKey]: T }>(
token
);
if (!data) return undefined;
// TODO: should validate if session is expired or not here, not in application code
return data[userSessionKey];
const data = await this.sessionProvider.getSession<T>(token);
return data;
}
async setSession(h3: H3Event, data: any, extend = false) {
const token =
this.getSessionToken(h3) ?? (await this.createSession(h3, extend));
const result = await this.sessionProvider.updateSession(
token,
userSessionKey,
data
);
return result;
}
async clearSession(request: MinimumRequestObject) {
const token = this.getSessionToken(request);
/**
* Signout session associated with request and deauthenticates it
* @param request
* @returns
*/
async signout(h3: H3Event) {
const token = this.getSessionToken(h3);
if (!token) return false;
await this.sessionProvider.clearSession(token);
const res = await this.sessionProvider.removeSession(token);
if (!res) return false;
deleteCookie(h3, dropTokenCookieName);
return true;
}
async getUserId(h3: MinimumRequestObject) {
const token = this.getSessionToken(h3);
if (!token) return undefined;
return await this.getUserIdRaw(token);
}
async getUserIdRaw(token: string) {
const session = await this.sessionProvider.getSession<{
[userIdKey]: string | undefined;
}>(token);
if (!session) return undefined;
return session[userIdKey];
async cleanupSessions() {
await this.sessionProvider.cleanupSessions();
}
async setUserId(h3: H3Event, userId: string, extend = false) {
const token =
this.getSessionToken(h3) ?? (await this.createSession(h3, extend));
/**
* Update session info
* @param token session token
* @param data new session data
* @returns success or not
*/
private async updateSession(token: string, data: Session) {
return await this.sessionProvider.updateSession(token, data);
}
const result = await this.sessionProvider.updateSession(
token,
userIdKey,
userId
);
// ---------------------- Private API Below ------------------------
/**
* Get session token on a request
* @param request
* @returns session token
*/
private getSessionToken(
request: MinimumRequestObject | undefined
): string | undefined {
if (!request) throw new Error("Native web request not available");
const cookieHeader = request.headers.get("Cookie");
if (!cookieHeader) return undefined;
const cookies = parseCookies(cookieHeader);
const cookie = cookies[dropTokenCookieName];
return cookie;
}
private createExipreAt(rememberMe: boolean) {
return moment()
.add(...(rememberMe ? extendedSessionLength : normalSessionLength))
.toDate();
}
/**
* Creates cookie that represents user session
* @param h3
* @param extend
* @returns
*/
private createSessionCookie(h3: H3Event, expiresAt: Date) {
const token = randomUUID();
// TODO: we should probably switch to jwts to minimize possibility of someone
// trying to guess a session id (jwts let us sign + encrypt stuff in a std way)
setCookie(h3, dropTokenCookieName, token, { expires: expiresAt });
return token;
}
}

View File

@@ -8,15 +8,23 @@ export default function createMemorySessionHandler() {
sessions[token] = data;
return true;
},
async updateSession(token, key, data) {
sessions[token] = Object.assign({}, sessions[token], { [key]: data });
async getSession<T extends Session>(token: string): Promise<T | undefined> {
const session = sessions[token];
return session ? (session as T) : undefined; // Ensure undefined is returned if session is not found
},
async updateSession(token, data) {
return this.setSession(token, data);
},
async removeSession(token) {
delete sessions[token];
return true;
},
async getSession(token) {
return sessions[token] as any; // Wild type cast because we let the user specify types if they want
},
async clearSession(token) {
delete sessions[token];
async cleanupSessions() {
const now = new Date();
for (let token in sessions) {
// if expires at time is before now, the session is expired
if (sessions[token].expiresAt < now) await this.removeSession(token);
}
},
};

View File

@@ -1,10 +1,17 @@
import { H3Event } from "h3";
export type Session = { [key: string]: any };
export type Session = {
userId: string;
expiresAt: Date;
data: {
[key: string]: any;
};
};
export interface SessionProvider {
setSession: (token: string, data: Session) => Promise<boolean>;
updateSession: (token: string, key: string, data: any) => Promise<boolean>;
getSession: <T extends Session>(token: string) => Promise<T | undefined>;
clearSession: (token: string) => Promise<void>;
setSession: (token: string, data: Session) => Promise<boolean>;
updateSession: (token: string, data: Session) => Promise<boolean>;
removeSession: (token: string) => Promise<boolean>;
cleanupSessions: () => Promise<void>;
}

View File

@@ -1,6 +1,9 @@
import { CertificateAuthority } from "../internal/clients/ca";
import fs from "fs";
import { fsCertificateStore } from "../internal/clients/ca-store";
import {
dbCertificateStore,
fsCertificateStore,
} from "../internal/clients/ca-store";
let ca: CertificateAuthority | undefined;
@@ -10,9 +13,9 @@ export const useCertificateAuthority = () => {
};
export default defineNitroPlugin(async (nitro) => {
const basePath = process.env.CLIENT_CERTIFICATES ?? "./certs";
fs.mkdirSync(basePath, { recursive: true });
const store = fsCertificateStore(basePath);
// const basePath = process.env.CLIENT_CERTIFICATES ?? "./certs";
// fs.mkdirSync(basePath, { recursive: true });
// const store = fsCertificateStore(basePath);
ca = await CertificateAuthority.new(store);
ca = await CertificateAuthority.new(dbCertificateStore());
});

View File

@@ -14,8 +14,8 @@ export default defineNitroPlugin((nitro) => {
switch (error.statusCode) {
case 401:
case 403:
const userId = await sessionHandler.getUserId(event);
if (userId) break;
const user = await sessionHandler.getSession(event);
if (user) break;
return sendRedirect(
event,
`/auth/signin?redirect=${encodeURIComponent(event.path)}`

View File

@@ -0,0 +1,12 @@
import sessionHandler from "~/server/internal/session";
export default defineTask({
meta: {
name: "cleanup:invitations",
},
async run({}) {
await sessionHandler.cleanupSessions();
return { result: true };
},
});

View File

@@ -1855,6 +1855,27 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b"
integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==
"@volar/language-core@2.4.12", "@volar/language-core@~2.4.11":
version "2.4.12"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.12.tgz#98c8424f8d81a9cad1760a587b1c6db27d05f0cc"
integrity sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==
dependencies:
"@volar/source-map" "2.4.12"
"@volar/source-map@2.4.12":
version "2.4.12"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.12.tgz#7cc8c6b1b134a2215f06c91ad011d94eef81b0ed"
integrity sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==
"@volar/typescript@~2.4.11":
version "2.4.12"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.12.tgz#8c638c23cab89ab131cdcd2d6f2a51768caaa015"
integrity sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==
dependencies:
"@volar/language-core" "2.4.12"
path-browserify "^1.0.1"
vscode-uri "^3.0.8"
"@vue-macros/common@^1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@vue-macros/common/-/common-1.16.1.tgz#dac7ebc57ded4d6fb19d7f9a83d2973971d9fa65"
@@ -1909,7 +1930,7 @@
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.5.13", "@vue/compiler-dom@^3.3.4":
"@vue/compiler-dom@3.5.13", "@vue/compiler-dom@^3.3.4", "@vue/compiler-dom@^3.5.0":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
@@ -1940,6 +1961,14 @@
"@vue/compiler-dom" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/compiler-vue2@^2.7.16":
version "2.7.16"
resolved "https://registry.yarnpkg.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz#2ba837cbd3f1b33c2bc865fbe1a3b53fb611e249"
integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
"@vue/devtools-api@^6.6.4":
version "6.6.4"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
@@ -1990,6 +2019,20 @@
dependencies:
rfdc "^1.4.1"
"@vue/language-core@2.2.8":
version "2.2.8"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.2.8.tgz#05befa390399fbd4409bc703ee0520b8ac1b7088"
integrity sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==
dependencies:
"@volar/language-core" "~2.4.11"
"@vue/compiler-dom" "^3.5.0"
"@vue/compiler-vue2" "^2.7.16"
"@vue/shared" "^3.5.0"
alien-signals "^1.0.3"
minimatch "^9.0.3"
muggle-string "^0.4.1"
path-browserify "^1.0.1"
"@vue/reactivity@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
@@ -2023,7 +2066,7 @@
"@vue/compiler-ssr" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/shared@3.5.13", "@vue/shared@^3.5.13":
"@vue/shared@3.5.13", "@vue/shared@^3.5.0", "@vue/shared@^3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
@@ -2068,6 +2111,11 @@ agent-base@^7.1.2:
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1"
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
alien-signals@^1.0.3:
version "1.0.13"
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-1.0.13.tgz#8d6db73462f742ee6b89671fbd8c37d0b1727a7e"
integrity sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==
ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -2900,6 +2948,11 @@ db0@^0.3.1:
resolved "https://registry.yarnpkg.com/db0/-/db0-0.3.1.tgz#84366f06cd9a154545b077be5cb955e4ac278314"
integrity sha512-3RogPLE2LLq6t4YiFCREyl572aBjkfMvfwPyN51df00TbPbryL3XqBYuJ/j6mgPssPK8AKfYdLxizaO5UG10sA==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -3787,6 +3840,11 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hookable@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
@@ -4447,6 +4505,11 @@ lru-cache@^10.2.0, lru-cache@^10.4.3:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117"
integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -4778,7 +4841,7 @@ minimatch@^5.1.0:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
minimatch@^9.0.3, minimatch@^9.0.4:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
@@ -4878,6 +4941,11 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
muggle-string@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
@@ -5335,6 +5403,11 @@ parseurl@^1.3.2, parseurl@~1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -6774,6 +6847,11 @@ type-level-regexp@~0.1.17:
resolved "https://registry.yarnpkg.com/type-level-regexp/-/type-level-regexp-0.1.17.tgz#ec1bf7dd65b85201f9863031d6f023bdefc2410f"
integrity sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==
typescript@^5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
ufo@^1.1.2, ufo@^1.3.2, ufo@^1.4.0, ufo@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
@@ -7189,7 +7267,7 @@ vscode-languageserver@^7.0.0:
dependencies:
vscode-languageserver-protocol "3.16.0"
vscode-uri@^3.0.2:
vscode-uri@^3.0.2, vscode-uri@^3.0.8:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
@@ -7213,6 +7291,14 @@ vue-router@^4.5.0, vue-router@latest:
dependencies:
"@vue/devtools-api" "^6.6.4"
vue-tsc@^2.2.8:
version "2.2.8"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.2.8.tgz#7c8e1bd9333d25241a7f9988eedf08c65483158c"
integrity sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ==
dependencies:
"@volar/typescript" "~2.4.11"
"@vue/language-core" "2.2.8"
vue3-carousel-nuxt@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/vue3-carousel-nuxt/-/vue3-carousel-nuxt-1.1.5.tgz#c12d521d2ab16da7cbe3778d097262cfca1117c0"