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:
DecDuck
2024-10-09 14:43:06 +11:00
parent de388a937a
commit 435551c207
20 changed files with 376 additions and 63 deletions

View File

@ -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=""

View File

@ -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 });
};

View File

@ -1,4 +1,4 @@
const whitelistedPrefixes = ["/signin", "/register"];
const whitelistedPrefixes = ["/signin", "/register", "/api"];
export default defineNuxtRouteMiddleware(async (to, from) => {
if (import.meta.server) return;

View File

@ -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",

View File

@ -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>

View File

@ -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({

View File

@ -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;

View File

@ -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[]

View File

@ -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;
});

View File

@ -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

View 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;
});

View 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
});

View File

@ -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
View File

@ -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
}
}

View File

@ -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({

View 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;
}
}

View File

@ -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;
})

View File

@ -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
View 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
View File

@ -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"