mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
object storage + full permission system + testing
Object storage now works fully, with the permission system. It still needs additional external endpoints for updating and deleting objects from the API, but it is otherwise complete. Further tasks include writing an S3 adapter.
This commit is contained in:
@ -2,4 +2,7 @@ DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop"
|
||||
|
||||
CLIENT_CERTIFICATES="./.data/ca"
|
||||
|
||||
GIANT_BOMB_API_KEY=""data
|
||||
FS_BACKEND_PATH="./.data/objects"
|
||||
|
||||
GIANT_BOMB_API_KEY=""
|
||||
|
||||
|
||||
@ -12,5 +12,5 @@ export const updateUser = async () => {
|
||||
if (user.value === null) return;
|
||||
|
||||
// SSR calls have to be after uses
|
||||
user.value = await $fetch<User | null>("/api/v1/whoami", { headers });
|
||||
user.value = await $fetch<User | null>("/api/v1/user", { headers });
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const whitelistedPrefixes = ["/signin", "/register"];
|
||||
const whitelistedPrefixes = ["/signin", "/register", "/api"];
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
if (import.meta.server) return;
|
||||
|
||||
@ -16,10 +16,13 @@
|
||||
"@prisma/client": "5.20.0",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "^5.1.1",
|
||||
"file-type-mime": "^0.4.3",
|
||||
"moment": "^2.30.1",
|
||||
"nuxt": "^3.13.0",
|
||||
"prisma": "^5.20.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"stream": "^0.0.3",
|
||||
"stream-mime-type": "^2.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "latest",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<form @submit.prevent="register">
|
||||
<input type="text" v-model="username" placeholder="username" />
|
||||
<input type="text" v-model="password" placeholder="password" />
|
||||
<input type="password" v-model="password" placeholder="password" />
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
@ -159,7 +159,7 @@ async function signin() {
|
||||
},
|
||||
});
|
||||
const user = useUser();
|
||||
user.value = await $fetch<User | null>("/api/v1/whoami");
|
||||
user.value = await $fetch<User | null>("/api/v1/user");
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `displayName` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `email` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `profilePicture` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "displayName" TEXT NOT NULL,
|
||||
ADD COLUMN "email" TEXT NOT NULL,
|
||||
ADD COLUMN "profilePicture" TEXT NOT NULL;
|
||||
@ -18,8 +18,9 @@ model User {
|
||||
username String @unique
|
||||
admin Boolean @default(false)
|
||||
|
||||
email String
|
||||
displayName String
|
||||
email String
|
||||
displayName String
|
||||
profilePicture String // Object
|
||||
|
||||
authMecs LinkedAuthMec[]
|
||||
clients Client[]
|
||||
|
||||
@ -1,32 +1,60 @@
|
||||
import { AuthMec } from "@prisma/client";
|
||||
import { Readable } from "stream";
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import { createHash } from "~/server/internal/security/simple";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const body = await readBody(h3);
|
||||
const body = await readBody(h3);
|
||||
|
||||
const username = body.username;
|
||||
const password = body.password;
|
||||
if (username === undefined || password === undefined)
|
||||
throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." });
|
||||
|
||||
const existing = await prisma.user.count({ where: { username: username } });
|
||||
if (existing > 0) throw createError({ statusCode: 400, statusMessage: "Username already taken." })
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
}
|
||||
const username = body.username;
|
||||
const password = body.password;
|
||||
if (username === undefined || password === undefined)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Username or password missing from request.",
|
||||
});
|
||||
|
||||
const hash = await createHash(password);
|
||||
const authMek = await prisma.linkedAuthMec.create({
|
||||
data: {
|
||||
mec: AuthMec.Simple,
|
||||
credentials: [username, hash],
|
||||
userId: user.id
|
||||
}
|
||||
const existing = await prisma.user.count({ where: { username: username } });
|
||||
if (existing > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Username already taken.",
|
||||
});
|
||||
|
||||
return user;
|
||||
})
|
||||
const userId = uuidv4();
|
||||
|
||||
const profilePictureObject = await h3.context.objects.createFromSource(
|
||||
() =>
|
||||
$fetch<Readable>("https://avatars.githubusercontent.com/u/64579723?v=4", {
|
||||
responseType: "stream",
|
||||
}),
|
||||
{},
|
||||
[`anonymous:read`, `${userId}:write`]
|
||||
);
|
||||
if (!profilePictureObject)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Unable to import profile picture",
|
||||
});
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
displayName: "",
|
||||
email: "",
|
||||
profilePicture: profilePictureObject,
|
||||
},
|
||||
});
|
||||
|
||||
const hash = await createHash(password);
|
||||
await prisma.linkedAuthMec.create({
|
||||
data: {
|
||||
mec: AuthMec.Simple,
|
||||
credentials: [username, hash],
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import clientHandler from "~/server/internal/clients/handler";
|
||||
import { useGlobalCertificateAuthority } from "~/server/plugins/ca";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const body = await readBody(h3);
|
||||
@ -28,7 +27,7 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Invalid token",
|
||||
});
|
||||
|
||||
const ca = useGlobalCertificateAuthority();
|
||||
const ca = h3.context.ca;
|
||||
const bundle = await ca.generateClientCertificate(
|
||||
clientId,
|
||||
metadata.data.name
|
||||
|
||||
13
server/api/v1/object/[id]/index.get.ts
Normal file
13
server/api/v1/object/[id]/index.get.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const id = getRouterParam(h3, "id");
|
||||
if (!id) throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
|
||||
|
||||
const userId = await h3.context.session.getUserId(h3);
|
||||
|
||||
const object = await h3.context.objects.fetchWithPermissions(id, userId);
|
||||
if (!object)
|
||||
throw createError({ statusCode: 404, statusMessage: "Object not found" });
|
||||
|
||||
setHeader(h3, "Content-Type", object.mime);
|
||||
return object.data;
|
||||
});
|
||||
4
server/api/v1/user/index.get.ts
Normal file
4
server/api/v1/user/index.get.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await h3.context.session.getUser(h3);
|
||||
return user ?? null; // Need to specifically return null
|
||||
});
|
||||
@ -1,4 +0,0 @@
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await h3.context.session.getUser(h3);
|
||||
return user ?? null;
|
||||
});
|
||||
12
server/h3.d.ts
vendored
12
server/h3.d.ts
vendored
@ -1,10 +1,14 @@
|
||||
import { CertificateAuthority } from "./internal/clients/ca";
|
||||
import { MetadataHandler } from "./internal/metadata";
|
||||
import { ObjectBackend } from "./internal/objects";
|
||||
import { SessionHandler } from "./internal/session";
|
||||
|
||||
export * from "h3";
|
||||
declare module "h3" {
|
||||
interface H3EventContext {
|
||||
session: SessionHandler;
|
||||
metadataHandler: MetadataHandler;
|
||||
}
|
||||
interface H3EventContext {
|
||||
session: SessionHandler;
|
||||
metadataHandler: MetadataHandler;
|
||||
ca: CertificateAuthority;
|
||||
objects: ObjectBackend
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Client, User } from "@prisma/client";
|
||||
import { EventHandlerRequest, H3Event } from "h3";
|
||||
import droplet from "@drop/droplet";
|
||||
import { useGlobalCertificateAuthority } from "~/server/plugins/ca";
|
||||
import prisma from "../db/database";
|
||||
|
||||
export type EventHandlerFunction<T> = (
|
||||
@ -31,7 +30,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
if (!clientId || !nonce || !signature)
|
||||
throw createError({ statusCode: 403 });
|
||||
|
||||
const ca = useGlobalCertificateAuthority();
|
||||
const ca = h3.context.ca;
|
||||
const certBundle = await ca.fetchClientCertificate(clientId);
|
||||
if (!certBundle)
|
||||
throw createError({
|
||||
|
||||
100
server/internal/objects/fsBackend.ts
Normal file
100
server/internal/objects/fsBackend.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { Object, ObjectBackend, ObjectMetadata, ObjectReference, Source } from ".";
|
||||
|
||||
import sanitize from "sanitize-filename";
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Readable, Stream } from "stream";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export class FsObjectBackend extends ObjectBackend {
|
||||
private baseObjectPath: string;
|
||||
private baseMetadataPath: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const basePath = process.env.FS_BACKEND_PATH ?? "./.data/objects";
|
||||
this.baseObjectPath = path.join(basePath, "objects");
|
||||
this.baseMetadataPath = path.join(basePath, "metadata");
|
||||
|
||||
fs.mkdirSync(this.baseObjectPath, { recursive: true });
|
||||
fs.mkdirSync(this.baseMetadataPath, { recursive: true });
|
||||
}
|
||||
|
||||
async fetch(id: ObjectReference) {
|
||||
const objectPath = path.join(this.baseObjectPath, sanitize(id));
|
||||
if (!fs.existsSync(objectPath)) return undefined;
|
||||
return fs.createReadStream(objectPath);
|
||||
}
|
||||
async write(id: ObjectReference, source: Source): Promise<boolean> {
|
||||
const objectPath = path.join(this.baseObjectPath, sanitize(id));
|
||||
if (!fs.existsSync(objectPath)) return false;
|
||||
|
||||
if (source instanceof Readable) {
|
||||
const outputStream = fs.createWriteStream(objectPath);
|
||||
source.pipe(outputStream, { end: true });
|
||||
await new Promise((r, j) => source.on("end", r));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (source instanceof Buffer) {
|
||||
fs.writeFileSync(objectPath, source);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
async create(
|
||||
source: Source,
|
||||
metadata: ObjectMetadata
|
||||
): Promise<ObjectReference | undefined> {
|
||||
const id = uuidv4();
|
||||
const objectPath = path.join(this.baseObjectPath, sanitize(id));
|
||||
const metadataPath = path.join(
|
||||
this.baseMetadataPath,
|
||||
`${sanitize(id)}.json`
|
||||
);
|
||||
if (fs.existsSync(objectPath) || fs.existsSync(metadataPath))
|
||||
return undefined;
|
||||
|
||||
// Write metadata
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
|
||||
|
||||
// Create file so write passes
|
||||
fs.writeFileSync(objectPath, "");
|
||||
|
||||
// Call write
|
||||
this.write(id, source);
|
||||
|
||||
return id;
|
||||
}
|
||||
async delete(id: ObjectReference): Promise<boolean> {
|
||||
const objectPath = path.join(this.baseObjectPath, sanitize(id));
|
||||
if (!fs.existsSync(objectPath)) return true;
|
||||
fs.rmSync(objectPath);
|
||||
return true;
|
||||
}
|
||||
async fetchMetadata(
|
||||
id: ObjectReference
|
||||
): Promise<ObjectMetadata | undefined> {
|
||||
const metadataPath = path.join(
|
||||
this.baseMetadataPath,
|
||||
`${sanitize(id)}.json`
|
||||
);
|
||||
if (!fs.existsSync(metadataPath)) return undefined;
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||
return metadata as ObjectMetadata;
|
||||
}
|
||||
async writeMetadata(
|
||||
id: ObjectReference,
|
||||
metadata: ObjectMetadata
|
||||
): Promise<boolean> {
|
||||
const metadataPath = path.join(
|
||||
this.baseMetadataPath,
|
||||
`${sanitize(id)}.json`
|
||||
);
|
||||
if (!fs.existsSync(metadataPath)) return false;
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,9 @@
|
||||
* anotherUserId:write
|
||||
*/
|
||||
|
||||
import { parse as getMimeTypeBuffer } from "file-type-mime";
|
||||
import { Readable } from "stream";
|
||||
import { getMimeType as getMimeTypeStream } from "stream-mime-type";
|
||||
|
||||
export type ObjectReference = string;
|
||||
export type ObjectMetadata = {
|
||||
@ -34,16 +36,14 @@ export const ObjectPermissionPriority: Array<ObjectPermission> = [
|
||||
ObjectPermission.Delete,
|
||||
];
|
||||
|
||||
export type Source = Readable | Buffer;
|
||||
export type Object = { mime: string; data: Source };
|
||||
|
||||
export type Object = { metadata: ObjectMetadata };
|
||||
export type StreamObject = Object & { stream: Readable };
|
||||
export type BufferObject = Object & { buffer: Buffer };
|
||||
export type Source = Readable | Buffer;
|
||||
|
||||
export abstract class ObjectBackend {
|
||||
// Interface functions, not designed to be called directly.
|
||||
// They don't check permissions to provide any utilities
|
||||
abstract fetch(id: ObjectReference): Promise<Object | undefined>;
|
||||
abstract fetch(id: ObjectReference): Promise<Source | undefined>;
|
||||
abstract write(id: ObjectReference, source: Source): Promise<boolean>;
|
||||
abstract create(
|
||||
source: Source,
|
||||
@ -58,13 +58,48 @@ export abstract class ObjectBackend {
|
||||
metadata: ObjectMetadata
|
||||
): Promise<boolean>;
|
||||
|
||||
async fetchWithPermissions(id: ObjectReference, userId: string) {
|
||||
async createFromSource(
|
||||
sourceFetcher: () => Promise<Source>,
|
||||
metadata: { [key: string]: string },
|
||||
permissions: Array<string>
|
||||
) {
|
||||
async function fetchMimeType(source: Source) {
|
||||
if (source instanceof ReadableStream) {
|
||||
source = Readable.from(source);
|
||||
}
|
||||
if (source instanceof Readable) {
|
||||
const { stream, mime } = await getMimeTypeStream(source);
|
||||
return { source: Readable.from(stream), mime: mime };
|
||||
}
|
||||
if (source instanceof Buffer) {
|
||||
const mime =
|
||||
getMimeTypeBuffer(source)?.mime ?? "application/octet-stream";
|
||||
return { source: source, mime };
|
||||
}
|
||||
|
||||
return { source: undefined, mime: undefined };
|
||||
}
|
||||
const { source, mime } = await fetchMimeType(await sourceFetcher());
|
||||
if (!mime)
|
||||
throw new Error("Unable to calculate MIME type - is the source empty?");
|
||||
|
||||
const objectId = this.create(source, {
|
||||
permissions,
|
||||
userMetadata: metadata,
|
||||
mime,
|
||||
});
|
||||
|
||||
return objectId;
|
||||
}
|
||||
|
||||
async fetchWithPermissions(id: ObjectReference, userId?: string) {
|
||||
const metadata = await this.fetchMetadata(id);
|
||||
if (!metadata) return;
|
||||
|
||||
// We only need one permission, so find instead of filter is faster
|
||||
const myPermissions = metadata.permissions.find((e) => {
|
||||
if (e.startsWith(userId)) return true;
|
||||
if (userId !== undefined && e.startsWith(userId)) return true;
|
||||
if (userId !== undefined && e.startsWith("internal")) return true;
|
||||
if (e.startsWith("anonymous")) return true;
|
||||
return false;
|
||||
});
|
||||
@ -76,7 +111,13 @@ export abstract class ObjectBackend {
|
||||
|
||||
// Because any permission can be read or up, we automatically know we can read this object
|
||||
// So just straight return the object
|
||||
return await this.fetch(id);
|
||||
const source = await this.fetch(id);
|
||||
if (!source) return undefined;
|
||||
const object: Object = {
|
||||
data: source,
|
||||
mime: metadata.mime,
|
||||
};
|
||||
return object;
|
||||
}
|
||||
|
||||
// If we need to fetch a remote resource, it doesn't make sense
|
||||
@ -87,14 +128,14 @@ export abstract class ObjectBackend {
|
||||
async writeWithPermissions(
|
||||
id: ObjectReference,
|
||||
sourceFetcher: () => Promise<Source>,
|
||||
userId: string
|
||||
userId?: string
|
||||
) {
|
||||
const metadata = await this.fetchMetadata(id);
|
||||
if (!metadata) return;
|
||||
|
||||
const myPermissions = metadata.permissions
|
||||
.filter((e) => {
|
||||
if (e.startsWith(userId)) return true;
|
||||
if (userId !== undefined && e.startsWith(userId)) return true;
|
||||
if (e.startsWith("anonymous")) return true;
|
||||
return false;
|
||||
})
|
||||
@ -115,13 +156,14 @@ export abstract class ObjectBackend {
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteWithPermission(id: ObjectReference, userId: string) {
|
||||
async deleteWithPermission(id: ObjectReference, userId?: string) {
|
||||
const metadata = await this.fetchMetadata(id);
|
||||
if (!metadata) return false;
|
||||
|
||||
const myPermissions = metadata.permissions
|
||||
.filter((e) => {
|
||||
if (e.startsWith(userId)) return true;
|
||||
if (userId !== undefined && e.startsWith(userId)) return true;
|
||||
if (userId !== undefined && e.startsWith("internal")) return true;
|
||||
if (e.startsWith("anonymous")) return true;
|
||||
return false;
|
||||
})
|
||||
|
||||
@ -15,4 +15,13 @@ export default defineNitroPlugin(async (nitro) => {
|
||||
const store = fsCertificateStore(basePath);
|
||||
|
||||
ca = await CertificateAuthority.new(store);
|
||||
|
||||
nitro.hooks.hook("request", (h3) => {
|
||||
if (!ca)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Certificate authority not initialised",
|
||||
});
|
||||
h3.context.ca = ca;
|
||||
});
|
||||
});
|
||||
|
||||
11
server/plugins/objects.ts
Normal file
11
server/plugins/objects.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { FsObjectBackend } from "../internal/objects/fsBackend";
|
||||
|
||||
export default defineNitroPlugin((nitro) => {
|
||||
const currentObjectHandler = new FsObjectBackend();
|
||||
|
||||
// To-do insert logic surrounding deciding what object backend to use
|
||||
|
||||
nitro.hooks.hook("request", (h3) => {
|
||||
h3.context.objects = currentObjectHandler;
|
||||
});
|
||||
});
|
||||
109
yarn.lock
109
yarn.lock
@ -1316,6 +1316,11 @@
|
||||
dependencies:
|
||||
"@tanstack/virtual-core" "3.10.8"
|
||||
|
||||
"@tokenizer/token@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
|
||||
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
|
||||
|
||||
"@trysound/sax@0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
||||
@ -2732,6 +2737,20 @@ fdir@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.3.0.tgz#fcca5a23ea20e767b15e081ee13b3e6488ee0bb0"
|
||||
integrity sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==
|
||||
|
||||
file-type-mime@^0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/file-type-mime/-/file-type-mime-0.4.3.tgz#04db9670dba39d37525f0367dc327f2b8e9f6c04"
|
||||
integrity sha512-yumBt0l9E03Oyk3KZyq9KTM9LF0XClKWtVU+bDEOl+tbIlUr/Jnl0ZjkF/r6KmqmjJgGaWhUDSdG2HUvLJ3kNA==
|
||||
|
||||
file-type@^18.2.0:
|
||||
version "18.7.0"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-18.7.0.tgz#cddb16f184d6b94106cfc4bb56978726b25cb2a2"
|
||||
integrity sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==
|
||||
dependencies:
|
||||
readable-web-to-node-stream "^3.0.2"
|
||||
strtok3 "^7.0.0"
|
||||
token-types "^5.0.1"
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
@ -3505,7 +3524,7 @@ mime-db@1.52.0:
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
mime-types@^2.1.12, mime-types@^2.1.35:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
@ -4052,6 +4071,11 @@ pathe@^1.1.1, pathe@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
|
||||
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
|
||||
|
||||
peek-readable@^5.1.3:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.2.0.tgz#7458f18126217c154938c32a185f5d05f3df3710"
|
||||
integrity sha512-U94a+eXHzct7vAd19GH3UQ2dH4Satbng0MyYTMaQatL0pvYYL5CTPR25HBhKtecl+4bfu1/i3vC6k0hydO5Vcw==
|
||||
|
||||
perfect-debounce@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
||||
@ -4427,6 +4451,15 @@ read-cache@^1.0.0:
|
||||
dependencies:
|
||||
pify "^2.3.0"
|
||||
|
||||
readable-stream@3, readable-stream@^3.6.0:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
|
||||
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@^2.0.5:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
|
||||
@ -4440,15 +4473,6 @@ readable-stream@^2.0.5:
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@^3.6.0:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
|
||||
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@^4.0.0:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09"
|
||||
@ -4460,6 +4484,13 @@ readable-stream@^4.0.0:
|
||||
process "^0.11.10"
|
||||
string_decoder "^1.3.0"
|
||||
|
||||
readable-web-to-node-stream@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb"
|
||||
integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==
|
||||
dependencies:
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
readdir-glob@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584"
|
||||
@ -4584,6 +4615,13 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
sanitize-filename@^1.6.3:
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
|
||||
integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
|
||||
dependencies:
|
||||
truncate-utf8-bytes "^1.0.0"
|
||||
|
||||
sass@^1.79.4:
|
||||
version "1.79.4"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.4.tgz#f9c45af35fbeb53d2c386850ec842098d9935267"
|
||||
@ -4764,6 +4802,22 @@ std-env@^3.7.0:
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
||||
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
|
||||
|
||||
stream-head@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stream-head/-/stream-head-3.0.0.tgz#cf69c14f3f6d8c63b1475a0e3ccc0ee58ddd2c1c"
|
||||
integrity sha512-EfcHQpe+HxwAY46J+o+LeQG8gL6FfxBBfNEGzPWzXYEiL2dRS1dtFJ2F38JLcrSKz1tIFA3HkST4SkTPA7+jgw==
|
||||
dependencies:
|
||||
through2 "4.0.2"
|
||||
|
||||
stream-mime-type@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stream-mime-type/-/stream-mime-type-2.0.0.tgz#584aeeea2f45bd1a732d8bd248f8bcfba1130bad"
|
||||
integrity sha512-JKka2v3YRBr0P2nWeJIWTBtMhYO1CHQp/TecKuSdz6+33Tm5Z5xH2raLalkVrRYloHa3FZKAe7DCVAQeRPYUGQ==
|
||||
dependencies:
|
||||
file-type "^18.2.0"
|
||||
mime-types "^2.1.35"
|
||||
stream-head "^3.0.0"
|
||||
|
||||
stream@^0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.3.tgz#3f3934a900a561ce3e2b9ffbd2819cead32699d9"
|
||||
@ -4856,6 +4910,14 @@ strip-literal@^2.1.0:
|
||||
dependencies:
|
||||
js-tokens "^9.0.0"
|
||||
|
||||
strtok3@^7.0.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-7.1.1.tgz#f548fd9dc59d0a76d5567ff8c16be31221f29dfc"
|
||||
integrity sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==
|
||||
dependencies:
|
||||
"@tokenizer/token" "^0.3.0"
|
||||
peek-readable "^5.1.3"
|
||||
|
||||
stylehacks@^7.0.4:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-7.0.4.tgz#9c21f7374f4bccc0082412b859b3c89d77d3277c"
|
||||
@ -5016,6 +5078,13 @@ thenify-all@^1.0.0:
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
through2@4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764"
|
||||
integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==
|
||||
dependencies:
|
||||
readable-stream "3"
|
||||
|
||||
tiny-invariant@^1.1.0:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
|
||||
@ -5046,6 +5115,14 @@ toidentifier@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
token-types@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/token-types/-/token-types-5.0.1.tgz#aa9d9e6b23c420a675e55413b180635b86a093b4"
|
||||
integrity sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==
|
||||
dependencies:
|
||||
"@tokenizer/token" "^0.3.0"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
totalist@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
|
||||
@ -5056,6 +5133,13 @@ tr46@~0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
truncate-utf8-bytes@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
|
||||
integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==
|
||||
dependencies:
|
||||
utf8-byte-length "^1.0.1"
|
||||
|
||||
ts-interface-checker@^0.1.9:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
||||
@ -5261,6 +5345,11 @@ urlpattern-polyfill@8.0.2:
|
||||
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz#99f096e35eff8bf4b5a2aa7d58a1523d6ebc7ce5"
|
||||
integrity sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==
|
||||
|
||||
utf8-byte-length@^1.0.1:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e"
|
||||
integrity sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
Reference in New Issue
Block a user