aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-07-04 23:58:42 +0100
committerGitHub <noreply@github.com>2025-07-04 23:58:42 +0100
commitd66b3b8619e8fff36c0243f7cc67eef864c5009b (patch)
tree6f555ad31cfc44aebffab1db3edb6134c10878d0
parent53b6b3c24d9669ba240c1f9c5fb58672b6cf8666 (diff)
downloadkarakeep-d66b3b8619e8fff36c0243f7cc67eef864c5009b.tar.zst
feat: Add support for S3 as an asset storage layer (#1703)
* feat: Add support for S3 as an asset storage layer. Fixes #305 * some minor fixes * use bulk deletion api * stream the file to s3
-rw-r--r--docs/docs/03-configuration.md41
-rw-r--r--packages/api/utils/assets.ts4
-rw-r--r--packages/api/utils/upload.ts2
-rw-r--r--packages/e2e_tests/docker-compose.yml15
-rw-r--r--packages/e2e_tests/package.json5
-rw-r--r--packages/e2e_tests/setup/startContainers.ts4
-rw-r--r--packages/e2e_tests/tests/assetdb/assetdb-utils.ts289
-rw-r--r--packages/e2e_tests/tests/assetdb/interface-compliance.test.ts627
-rw-r--r--packages/e2e_tests/tests/assetdb/local-filesystem-store.test.ts228
-rw-r--r--packages/e2e_tests/tests/assetdb/s3-store.test.ts197
-rw-r--r--packages/shared/assetdb.ts670
-rw-r--r--packages/shared/config.ts21
-rw-r--r--packages/shared/package.json1
-rw-r--r--pnpm-lock.yaml1199
14 files changed, 3194 insertions, 109 deletions
diff --git a/docs/docs/03-configuration.md b/docs/docs/03-configuration.md
index 65dc5741..156632d7 100644
--- a/docs/docs/03-configuration.md
+++ b/docs/docs/03-configuration.md
@@ -2,18 +2,39 @@
The app is mainly configured by environment variables. All the used environment variables are listed in [packages/shared/config.ts](https://github.com/karakeep-app/karakeep/blob/main/packages/shared/config.ts). The most important ones are:
-| Name | Required | Default | Description |
-| ------------------------- | ------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| DATA_DIR | Yes | Not set | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set. |
-| ASSETS_DIR | No | Not set | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`. |
-| NEXTAUTH_URL | Yes | Not set | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example. |
-| NEXTAUTH_SECRET | Yes | Not set | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. |
-| MEILI_ADDR | No | Not set | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`) |
-| MEILI_MASTER_KEY | Only in Prod and if search is enabled | Not set | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \| tr -dc 'A-Za-z0-9'` |
-| MAX_ASSET_SIZE_MB | No | 50 | Sets the maximum allowed asset size (in MB) to be uploaded |
-| DISABLE_NEW_RELEASE_CHECK | No | false | If set to true, latest release check will be disabled in the admin panel. |
+| Name | Required | Default | Description |
+| ------------------------- | ------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| DATA_DIR | Yes | Not set | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set. |
+| ASSETS_DIR | No | Not set | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`. |
+| NEXTAUTH_URL | Yes | Not set | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example. |
+| NEXTAUTH_SECRET | Yes | Not set | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. |
+| MEILI_ADDR | No | Not set | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`) |
+| MEILI_MASTER_KEY | Only in Prod and if search is enabled | Not set | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \| tr -dc 'A-Za-z0-9'` |
+| MAX_ASSET_SIZE_MB | No | 50 | Sets the maximum allowed asset size (in MB) to be uploaded |
+| DISABLE_NEW_RELEASE_CHECK | No | false | If set to true, latest release check will be disabled in the admin panel. |
+
+## Asset Storage
+
+Karakeep supports two storage backends for assets: local filesystem (default) and S3-compatible object storage. S3 storage is automatically detected when an S3 endpoint is passed.
+
+| Name | Required | Default | Description |
+| -------------------------------- | ----------------- | --------- | --------------------------------------------------------------------------------------------------------- |
+| ASSET_STORE_S3_ENDPOINT | No | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. |
+| ASSET_STORE_S3_REGION | No | Not set | The S3 region to use. |
+| ASSET_STORE_S3_BUCKET | Yes when using S3 | Not set | The S3 bucket name where assets will be stored. |
+| ASSET_STORE_S3_ACCESS_KEY_ID | Yes when using S3 | Not set | The S3 access key ID for authentication. |
+| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication. |
+| ASSET_STORE_S3_FORCE_PATH_STYLE | No | false | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. |
+:::info
+When using S3 storage, make sure the bucket exists and the provided credentials have the necessary permissions to read, write, and delete objects in the bucket.
+:::
+
+:::warning
+Switching between storage backends after data has been stored will require manual migration of existing assets. Plan your storage backend choice carefully before deploying.
+:::
+
## Authentication / Signup
By default, Karakeep uses the database to store users, but it is possible to also use OAuth.
diff --git a/packages/api/utils/assets.ts b/packages/api/utils/assets.ts
index d8a726a6..205e1a76 100644
--- a/packages/api/utils/assets.ts
+++ b/packages/api/utils/assets.ts
@@ -28,7 +28,7 @@ export async function serveAsset(c: Context, assetId: string, userId: string) {
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : size - 1;
- const fStream = createAssetReadStream({
+ const fStream = await createAssetReadStream({
userId,
assetId,
start,
@@ -43,7 +43,7 @@ export async function serveAsset(c: Context, assetId: string, userId: string) {
await stream.pipe(toWebReadableStream(fStream));
});
} else {
- const fStream = createAssetReadStream({
+ const fStream = await createAssetReadStream({
userId,
assetId,
});
diff --git a/packages/api/utils/upload.ts b/packages/api/utils/upload.ts
index d96a0f60..daff1fb9 100644
--- a/packages/api/utils/upload.ts
+++ b/packages/api/utils/upload.ts
@@ -24,7 +24,7 @@ export function webStreamToNode(
}
export function toWebReadableStream(
- nodeStream: fs.ReadStream,
+ nodeStream: NodeJS.ReadableStream,
): ReadableStream<Uint8Array> {
const reader = nodeStream as unknown as Readable;
diff --git a/packages/e2e_tests/docker-compose.yml b/packages/e2e_tests/docker-compose.yml
index e1fe46bb..64775f46 100644
--- a/packages/e2e_tests/docker-compose.yml
+++ b/packages/e2e_tests/docker-compose.yml
@@ -35,3 +35,18 @@ services:
restart: unless-stopped
volumes:
- ./setup/html:/usr/share/nginx/html
+ minio:
+ image: minio/minio:latest
+ restart: unless-stopped
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ environment:
+ MINIO_ROOT_USER: minioadmin
+ MINIO_ROOT_PASSWORD: minioadmin
+ command: server /data --console-address ":9001"
+ volumes:
+ - minio_data:/data
+
+volumes:
+ minio_data:
diff --git a/packages/e2e_tests/package.json b/packages/e2e_tests/package.json
index 3f110838..0cbb8fb3 100644
--- a/packages/e2e_tests/package.json
+++ b/packages/e2e_tests/package.json
@@ -11,10 +11,13 @@
"lint": "oxlint .",
"lint:fix": "oxlint . --fix",
"test": "vitest run",
- "test:watch": "vitest"
+ "test:watch": "vitest",
+ "test:no-build": "E2E_TEST_NO_BUILD=1 vitest run"
},
"dependencies": {
+ "@aws-sdk/client-s3": "^3.842.0",
"@karakeep/sdk": "workspace:*",
+ "@karakeep/shared": "workspace:^0.1.0",
"@karakeep/trpc": "workspace:^0.1.0",
"superjson": "^2.2.1"
},
diff --git a/packages/e2e_tests/setup/startContainers.ts b/packages/e2e_tests/setup/startContainers.ts
index 3086d1c8..87e812a2 100644
--- a/packages/e2e_tests/setup/startContainers.ts
+++ b/packages/e2e_tests/setup/startContainers.ts
@@ -33,8 +33,10 @@ export default async function ({ provide }: GlobalSetupContext) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const port = await getRandomPort();
+ const buildArg = process.env.E2E_TEST_NO_BUILD ? "" : "--build";
+
console.log(`Starting docker compose on port ${port}...`);
- execSync(`docker compose up --build -d`, {
+ execSync(`docker compose up ${buildArg} -d`, {
cwd: __dirname,
stdio: "inherit",
env: {
diff --git a/packages/e2e_tests/tests/assetdb/assetdb-utils.ts b/packages/e2e_tests/tests/assetdb/assetdb-utils.ts
new file mode 100644
index 00000000..a8e29ab4
--- /dev/null
+++ b/packages/e2e_tests/tests/assetdb/assetdb-utils.ts
@@ -0,0 +1,289 @@
+import * as fs from "fs";
+import * as os from "os";
+import * as path from "path";
+import {
+ CreateBucketCommand,
+ DeleteBucketCommand,
+ DeleteObjectCommand,
+ ListObjectsV2Command,
+ S3Client,
+} from "@aws-sdk/client-s3";
+
+import {
+ ASSET_TYPES,
+ AssetMetadata,
+ AssetStore,
+ LocalFileSystemAssetStore,
+ S3AssetStore,
+} from "@karakeep/shared/assetdb";
+
+export interface TestAssetData {
+ userId: string;
+ assetId: string;
+ content: Buffer;
+ metadata: AssetMetadata;
+}
+
+export function createTestAssetData(
+ overrides: Partial<TestAssetData> = {},
+): TestAssetData {
+ const defaultData: TestAssetData = {
+ userId: `user_${Math.random().toString(36).substring(7)}`,
+ assetId: `asset_${Math.random().toString(36).substring(7)}`,
+ content: Buffer.from(`Test content ${Math.random()}`),
+ metadata: {
+ contentType: ASSET_TYPES.TEXT_HTML,
+ fileName: "test.html",
+ },
+ };
+
+ return { ...defaultData, ...overrides };
+}
+
+export function createTestImageData(): TestAssetData {
+ // Create a minimal PNG image (1x1 pixel)
+ const pngData = Buffer.from([
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
+ 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
+ 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00,
+ 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0x0f, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0x5c, 0xc2, 0x8a, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x49,
+ 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
+ ]);
+
+ return createTestAssetData({
+ content: pngData,
+ metadata: {
+ contentType: ASSET_TYPES.IMAGE_PNG,
+ fileName: "test.png",
+ },
+ });
+}
+
+export function createTestPdfData(): TestAssetData {
+ // Create a minimal PDF
+ const pdfContent = `%PDF-1.4
+1 0 obj
+<<
+/Type /Catalog
+/Pages 2 0 R
+>>
+endobj
+
+2 0 obj
+<<
+/Type /Pages
+/Kids [3 0 R]
+/Count 1
+>>
+endobj
+
+3 0 obj
+<<
+/Type /Page
+/Parent 2 0 R
+/MediaBox [0 0 612 792]
+>>
+endobj
+
+xref
+0 4
+0000000000 65535 f
+0000000010 00000 n
+0000000053 00000 n
+0000000125 00000 n
+trailer
+<<
+/Size 4
+/Root 1 0 R
+>>
+startxref
+173
+%%EOF`;
+
+ return createTestAssetData({
+ content: Buffer.from(pdfContent),
+ metadata: {
+ contentType: ASSET_TYPES.APPLICATION_PDF,
+ fileName: "test.pdf",
+ },
+ });
+}
+
+export async function createTempDirectory(): Promise<string> {
+ const tempDir = await fs.promises.mkdtemp(
+ path.join(os.tmpdir(), "assetdb-test-"),
+ );
+ return tempDir;
+}
+
+export async function cleanupTempDirectory(tempDir: string): Promise<void> {
+ try {
+ await fs.promises.rm(tempDir, { recursive: true, force: true });
+ } catch (error) {
+ console.warn(`Failed to cleanup temp directory ${tempDir}:`, error);
+ }
+}
+
+export function createLocalFileSystemStore(
+ rootPath: string,
+): LocalFileSystemAssetStore {
+ return new LocalFileSystemAssetStore(rootPath);
+}
+
+export function createS3Store(bucketName: string): S3AssetStore {
+ const s3Client = new S3Client({
+ region: "us-east-1",
+ endpoint: "http://localhost:9000", // MinIO endpoint for testing
+ credentials: {
+ accessKeyId: "minioadmin",
+ secretAccessKey: "minioadmin",
+ },
+ forcePathStyle: true,
+ });
+
+ return new S3AssetStore(s3Client, bucketName);
+}
+
+export async function createTestBucket(bucketName: string): Promise<S3Client> {
+ const s3Client = new S3Client({
+ region: "us-east-1",
+ endpoint: "http://localhost:9000",
+ credentials: {
+ accessKeyId: "minioadmin",
+ secretAccessKey: "minioadmin",
+ },
+ forcePathStyle: true,
+ });
+
+ try {
+ await s3Client.send(new CreateBucketCommand({ Bucket: bucketName }));
+ } catch (error: unknown) {
+ const err = error as { name?: string };
+ if (
+ err.name !== "BucketAlreadyOwnedByYou" &&
+ err.name !== "BucketAlreadyExists"
+ ) {
+ throw error;
+ }
+ }
+
+ return s3Client;
+}
+
+export async function cleanupTestBucket(
+ s3Client: S3Client,
+ bucketName: string,
+): Promise<void> {
+ try {
+ // List and delete all objects
+ let continuationToken: string | undefined;
+ do {
+ const listResponse = await s3Client.send(
+ new ListObjectsV2Command({
+ Bucket: bucketName,
+ ContinuationToken: continuationToken,
+ }),
+ );
+
+ if (listResponse.Contents && listResponse.Contents.length > 0) {
+ const deletePromises = listResponse.Contents.map(
+ (obj: { Key?: string }) =>
+ s3Client.send(
+ new DeleteObjectCommand({
+ Bucket: bucketName,
+ Key: obj.Key!,
+ }),
+ ),
+ );
+ await Promise.all(deletePromises);
+ }
+
+ continuationToken = listResponse.NextContinuationToken;
+ } while (continuationToken);
+
+ // Delete the bucket
+ await s3Client.send(new DeleteBucketCommand({ Bucket: bucketName }));
+ } catch (error) {
+ console.warn(`Failed to cleanup S3 bucket ${bucketName}:`, error);
+ }
+}
+
+export async function createTempFile(
+ content: Buffer,
+ fileName: string,
+): Promise<string> {
+ const tempDir = await createTempDirectory();
+ const filePath = path.join(tempDir, fileName);
+ await fs.promises.writeFile(filePath, content);
+ return filePath;
+}
+
+export async function streamToBuffer(
+ stream: NodeJS.ReadableStream,
+): Promise<Buffer> {
+ const chunks: Buffer[] = [];
+ const readable = stream as AsyncIterable<Buffer>;
+
+ for await (const chunk of readable) {
+ chunks.push(chunk);
+ }
+
+ return Buffer.concat(chunks);
+}
+
+export function generateLargeBuffer(sizeInMB: number): Buffer {
+ const sizeInBytes = sizeInMB * 1024 * 1024;
+ const buffer = Buffer.alloc(sizeInBytes);
+
+ // Fill with some pattern to make it compressible but not empty
+ for (let i = 0; i < sizeInBytes; i++) {
+ buffer[i] = i % 256;
+ }
+
+ return buffer;
+}
+
+export async function assertAssetExists(
+ store: AssetStore,
+ userId: string,
+ assetId: string,
+): Promise<void> {
+ const { asset, metadata } = await store.readAsset({ userId, assetId });
+ if (!asset || !metadata) {
+ throw new Error(`Asset ${assetId} for user ${userId} does not exist`);
+ }
+}
+
+export async function assertAssetNotExists(
+ store: AssetStore,
+ userId: string,
+ assetId: string,
+): Promise<void> {
+ try {
+ await store.readAsset({ userId, assetId });
+ throw new Error(`Asset ${assetId} for user ${userId} should not exist`);
+ } catch (error: unknown) {
+ // Expected to throw
+ const err = error as { message?: string };
+ if (err.message?.includes("should not exist")) {
+ throw error;
+ }
+ }
+}
+
+export async function getAllAssetsArray(store: AssetStore): Promise<
+ {
+ userId: string;
+ assetId: string;
+ contentType: string;
+ fileName?: string | null;
+ size: number;
+ }[]
+> {
+ const assets = [];
+ for await (const asset of store.getAllAssets()) {
+ assets.push(asset);
+ }
+ return assets;
+}
diff --git a/packages/e2e_tests/tests/assetdb/interface-compliance.test.ts b/packages/e2e_tests/tests/assetdb/interface-compliance.test.ts
new file mode 100644
index 00000000..d5288c7a
--- /dev/null
+++ b/packages/e2e_tests/tests/assetdb/interface-compliance.test.ts
@@ -0,0 +1,627 @@
+import * as fs from "fs";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+
+import { ASSET_TYPES, AssetStore } from "@karakeep/shared/assetdb";
+
+import {
+ assertAssetExists,
+ assertAssetNotExists,
+ cleanupTempDirectory,
+ cleanupTestBucket,
+ createLocalFileSystemStore,
+ createS3Store,
+ createTempDirectory,
+ createTempFile,
+ createTestAssetData,
+ createTestBucket,
+ createTestImageData,
+ createTestPdfData,
+ generateLargeBuffer,
+ getAllAssetsArray,
+ streamToBuffer,
+} from "./assetdb-utils";
+
+interface TestContext {
+ store: AssetStore;
+ cleanup: () => Promise<void>;
+}
+
+async function createLocalContext(): Promise<TestContext> {
+ const tempDir = await createTempDirectory();
+ const store = createLocalFileSystemStore(tempDir);
+
+ return {
+ store,
+ cleanup: async () => {
+ await cleanupTempDirectory(tempDir);
+ },
+ };
+}
+
+async function createS3Context(): Promise<TestContext> {
+ const bucketName = `test-bucket-${Math.random().toString(36).substring(7)}`;
+ const s3Client = await createTestBucket(bucketName);
+ const store = createS3Store(bucketName);
+
+ return {
+ store,
+ cleanup: async () => {
+ await cleanupTestBucket(s3Client, bucketName);
+ },
+ };
+}
+
+describe.each([
+ { name: "LocalFileSystemAssetStore", createContext: createLocalContext },
+ { name: "S3AssetStore", createContext: createS3Context },
+])("AssetStore Interface Compliance - $name", ({ createContext }) => {
+ let context: TestContext;
+
+ beforeEach(async () => {
+ context = await createContext();
+ });
+
+ afterEach(async () => {
+ await context.cleanup();
+ });
+
+ describe("Basic CRUD Operations", () => {
+ it("should save and retrieve an asset", async () => {
+ const testData = createTestAssetData();
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { asset, metadata } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(asset).toEqual(testData.content);
+ expect(metadata).toEqual(testData.metadata);
+ });
+
+ it("should delete an asset", async () => {
+ const testData = createTestAssetData();
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ await assertAssetExists(context.store, testData.userId, testData.assetId);
+
+ await context.store.deleteAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ await assertAssetNotExists(
+ context.store,
+ testData.userId,
+ testData.assetId,
+ );
+ });
+
+ it("should get asset size", async () => {
+ const testData = createTestAssetData();
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const size = await context.store.getAssetSize({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(size).toBe(testData.content.length);
+ });
+
+ it("should read asset metadata", async () => {
+ const testData = createTestAssetData();
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const metadata = await context.store.readAssetMetadata({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(metadata).toEqual(testData.metadata);
+ });
+ });
+
+ describe("Streaming Operations", () => {
+ it("should create readable stream", async () => {
+ const testData = createTestAssetData();
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const stream = await context.store.createAssetReadStream({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ const streamedContent = await streamToBuffer(stream);
+ expect(streamedContent).toEqual(testData.content);
+ });
+
+ it("should support range requests in streams", async () => {
+ const content = Buffer.from("0123456789abcdef");
+ const testData = createTestAssetData({ content });
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const stream = await context.store.createAssetReadStream({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ start: 5,
+ end: 10,
+ });
+
+ const streamedContent = await streamToBuffer(stream);
+ expect(streamedContent.toString()).toBe("56789a");
+ });
+ });
+
+ describe("Asset Types Support", () => {
+ it("should support all required asset types", async () => {
+ const testCases = [
+ { contentType: ASSET_TYPES.IMAGE_JPEG, fileName: "test.jpg" },
+ { contentType: ASSET_TYPES.IMAGE_PNG, fileName: "test.png" },
+ { contentType: ASSET_TYPES.IMAGE_WEBP, fileName: "test.webp" },
+ { contentType: ASSET_TYPES.APPLICATION_PDF, fileName: "test.pdf" },
+ { contentType: ASSET_TYPES.TEXT_HTML, fileName: "test.html" },
+ { contentType: ASSET_TYPES.VIDEO_MP4, fileName: "test.mp4" },
+ ];
+
+ for (const { contentType, fileName } of testCases) {
+ const testData = createTestAssetData({
+ metadata: { contentType, fileName },
+ });
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { metadata } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(metadata.contentType).toBe(contentType);
+ expect(metadata.fileName).toBe(fileName);
+ }
+ });
+
+ it("should handle large assets", async () => {
+ const largeContent = generateLargeBuffer(5); // 5MB
+ const testData = createTestAssetData({ content: largeContent });
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { asset } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(asset.length).toBe(largeContent.length);
+ expect(asset).toEqual(largeContent);
+ });
+
+ it("should reject unsupported asset types", async () => {
+ const testData = createTestAssetData({
+ metadata: {
+ contentType: "unsupported/type",
+ fileName: "test.unsupported",
+ },
+ });
+
+ await expect(
+ context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ }),
+ ).rejects.toThrow("Unsupported asset type");
+ });
+ });
+
+ describe("Bulk Operations", () => {
+ it("should delete all user assets", async () => {
+ const userId = "bulk-test-user";
+ const testAssets = [
+ createTestAssetData({ userId }),
+ createTestAssetData({ userId }),
+ createTestAssetData({ userId }),
+ ];
+ const otherUserAsset = createTestAssetData(); // Different user
+
+ // Save all assets
+ await Promise.all([
+ ...testAssets.map((testData) =>
+ context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ }),
+ ),
+ context.store.saveAsset({
+ userId: otherUserAsset.userId,
+ assetId: otherUserAsset.assetId,
+ asset: otherUserAsset.content,
+ metadata: otherUserAsset.metadata,
+ }),
+ ]);
+
+ // Delete user assets
+ await context.store.deleteUserAssets({ userId });
+
+ // Verify user assets are deleted
+ for (const testData of testAssets) {
+ await assertAssetNotExists(
+ context.store,
+ testData.userId,
+ testData.assetId,
+ );
+ }
+
+ // Verify other user's asset still exists
+ await assertAssetExists(
+ context.store,
+ otherUserAsset.userId,
+ otherUserAsset.assetId,
+ );
+ });
+
+ it("should list all assets", async () => {
+ const testAssets = [
+ createTestAssetData(),
+ createTestImageData(),
+ createTestPdfData(),
+ ];
+
+ // Save all assets
+ await Promise.all(
+ testAssets.map((testData) =>
+ context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ }),
+ ),
+ );
+
+ const assets = await getAllAssetsArray(context.store);
+
+ expect(assets).toHaveLength(3);
+
+ // Verify all assets are present
+ const assetIds = assets.map((a) => a.assetId);
+ for (const testData of testAssets) {
+ expect(assetIds).toContain(testData.assetId);
+ }
+
+ // Verify asset structure
+ for (const asset of assets) {
+ expect(asset).toHaveProperty("userId");
+ expect(asset).toHaveProperty("assetId");
+ expect(asset).toHaveProperty("contentType");
+ expect(asset).toHaveProperty("size");
+ expect(typeof asset.size).toBe("number");
+ expect(asset.size).toBeGreaterThan(0);
+ }
+ });
+ });
+
+ describe("File Operations", () => {
+ it("should save asset from file and delete original file", async () => {
+ const testData = createTestAssetData();
+ const tempFile = await createTempFile(testData.content, "temp-asset.bin");
+
+ // Verify temp file exists before operation
+ expect(
+ await fs.promises
+ .access(tempFile)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ await context.store.saveAssetFromFile({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ assetPath: tempFile,
+ metadata: testData.metadata,
+ });
+
+ // Verify temp file was deleted
+ expect(
+ await fs.promises
+ .access(tempFile)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(false);
+
+ // Verify asset was saved correctly
+ const { asset, metadata } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(asset).toEqual(testData.content);
+ expect(metadata).toEqual(testData.metadata);
+ });
+ });
+
+ describe("Error Handling", () => {
+ it("should throw error for non-existent asset read", async () => {
+ await expect(
+ context.store.readAsset({
+ userId: "non-existent-user",
+ assetId: "non-existent-asset",
+ }),
+ ).rejects.toThrow();
+ });
+
+ it("should throw error for non-existent asset metadata", async () => {
+ await expect(
+ context.store.readAssetMetadata({
+ userId: "non-existent-user",
+ assetId: "non-existent-asset",
+ }),
+ ).rejects.toThrow();
+ });
+
+ it("should throw error for non-existent asset size", async () => {
+ await expect(
+ context.store.getAssetSize({
+ userId: "non-existent-user",
+ assetId: "non-existent-asset",
+ }),
+ ).rejects.toThrow();
+ });
+
+ it("should handle deleting non-existent asset gracefully", async () => {
+ // Filesystem implementation throws errors for non-existent files
+ await expect(
+ context.store.deleteAsset({
+ userId: "non-existent-user",
+ assetId: "non-existent-asset",
+ }),
+ ).resolves.not.toThrow();
+ });
+
+ it("should handle deletion of non-existent user directory gracefully", async () => {
+ // Should not throw error when user directory doesn't exist
+ await expect(
+ context.store.deleteUserAssets({ userId: "non-existent-user" }),
+ ).resolves.not.toThrow();
+ });
+
+ it("should handle non-existent asset stream appropriately", async () => {
+ const streamResult = context.store.createAssetReadStream({
+ userId: "non-existent-user",
+ assetId: "non-existent-asset",
+ });
+
+ await expect(streamResult).rejects.toThrow();
+ });
+ });
+
+ describe("Data Integrity", () => {
+ it("should preserve binary data integrity", async () => {
+ const testData = createTestImageData();
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { asset } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ // Verify exact binary match
+ expect(asset).toEqual(testData.content);
+
+ // Verify PNG header is intact
+ expect(asset[0]).toBe(0x89);
+ expect(asset[1]).toBe(0x50);
+ expect(asset[2]).toBe(0x4e);
+ expect(asset[3]).toBe(0x47);
+ });
+
+ it("should preserve metadata exactly", async () => {
+ const testData = createTestAssetData({
+ metadata: {
+ contentType: ASSET_TYPES.APPLICATION_PDF,
+ fileName: "test-document.pdf",
+ },
+ });
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { metadata } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(metadata).toEqual(testData.metadata);
+ expect(metadata.contentType).toBe(ASSET_TYPES.APPLICATION_PDF);
+ expect(metadata.fileName).toBe("test-document.pdf");
+ });
+
+ it("should handle null fileName correctly", async () => {
+ const testData = createTestAssetData({
+ metadata: {
+ contentType: ASSET_TYPES.TEXT_HTML,
+ fileName: null,
+ },
+ });
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { metadata } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(metadata.fileName).toBeNull();
+ });
+ });
+
+ describe("Concurrent Operations", () => {
+ it("should handle concurrent saves safely", async () => {
+ const testAssets = Array.from({ length: 5 }, () => createTestAssetData());
+
+ await Promise.all(
+ testAssets.map((testData) =>
+ context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ }),
+ ),
+ );
+
+ // Verify all assets were saved correctly
+ for (const testData of testAssets) {
+ const { asset, metadata } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(asset).toEqual(testData.content);
+ expect(metadata).toEqual(testData.metadata);
+ }
+ });
+
+ it("should handle concurrent reads safely", async () => {
+ const testData = createTestAssetData();
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ // Perform multiple concurrent reads
+ const readPromises = Array.from({ length: 10 }, () =>
+ context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ }),
+ );
+
+ const results = await Promise.all(readPromises);
+
+ // Verify all reads returned the same data
+ for (const { asset, metadata } of results) {
+ expect(asset).toEqual(testData.content);
+ expect(metadata).toEqual(testData.metadata);
+ }
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle empty assets", async () => {
+ const testData = createTestAssetData({
+ content: Buffer.alloc(0),
+ });
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { asset } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(asset.length).toBe(0);
+
+ const size = await context.store.getAssetSize({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(size).toBe(0);
+ });
+
+ it("should handle special characters in user and asset IDs", async () => {
+ const testData = createTestAssetData({
+ userId: "user-with-special_chars.123",
+ assetId: "asset_with-special.chars_456",
+ });
+
+ await context.store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const { asset, metadata } = await context.store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(asset).toEqual(testData.content);
+ expect(metadata).toEqual(testData.metadata);
+ });
+ });
+});
diff --git a/packages/e2e_tests/tests/assetdb/local-filesystem-store.test.ts b/packages/e2e_tests/tests/assetdb/local-filesystem-store.test.ts
new file mode 100644
index 00000000..36ff837f
--- /dev/null
+++ b/packages/e2e_tests/tests/assetdb/local-filesystem-store.test.ts
@@ -0,0 +1,228 @@
+import * as fs from "fs";
+import * as path from "path";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+
+import { LocalFileSystemAssetStore } from "@karakeep/shared/assetdb";
+
+import {
+ assertAssetNotExists,
+ cleanupTempDirectory,
+ createLocalFileSystemStore,
+ createTempDirectory,
+ createTestAssetData,
+} from "./assetdb-utils";
+
+describe("LocalFileSystemAssetStore - Filesystem-Specific Behaviors", () => {
+ let tempDir: string;
+ let store: LocalFileSystemAssetStore;
+
+ beforeEach(async () => {
+ tempDir = await createTempDirectory();
+ store = createLocalFileSystemStore(tempDir);
+ });
+
+ afterEach(async () => {
+ await cleanupTempDirectory(tempDir);
+ });
+
+ describe("File System Structure", () => {
+ it("should create correct directory structure and files", async () => {
+ const testData = createTestAssetData();
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ // Verify directory structure
+ const assetDir = path.join(tempDir, testData.userId, testData.assetId);
+ expect(
+ await fs.promises
+ .access(assetDir)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ // Verify asset.bin file
+ const assetFile = path.join(assetDir, "asset.bin");
+ expect(
+ await fs.promises
+ .access(assetFile)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ // Verify metadata.json file
+ const metadataFile = path.join(assetDir, "metadata.json");
+ expect(
+ await fs.promises
+ .access(metadataFile)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ // Verify file contents
+ const savedContent = await fs.promises.readFile(assetFile);
+ expect(savedContent).toEqual(testData.content);
+
+ const savedMetadata = JSON.parse(
+ await fs.promises.readFile(metadataFile, "utf8"),
+ );
+ expect(savedMetadata).toEqual(testData.metadata);
+ });
+
+ it("should create nested directory structure for user/asset hierarchy", async () => {
+ const userId = "user123";
+ const assetId = "asset456";
+ const testData = createTestAssetData({ userId, assetId });
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ // Verify the exact directory structure
+ const userDir = path.join(tempDir, userId);
+ const assetDir = path.join(userDir, assetId);
+
+ expect(
+ await fs.promises
+ .access(userDir)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ expect(
+ await fs.promises
+ .access(assetDir)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ // Verify files exist in the correct location
+ expect(
+ await fs.promises
+ .access(path.join(assetDir, "asset.bin"))
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ expect(
+ await fs.promises
+ .access(path.join(assetDir, "metadata.json"))
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+ });
+ });
+
+ describe("Directory Cleanup", () => {
+ it("should remove entire asset directory when deleting asset", async () => {
+ const testData = createTestAssetData();
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const assetDir = path.join(tempDir, testData.userId, testData.assetId);
+
+ // Verify directory exists
+ expect(
+ await fs.promises
+ .access(assetDir)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ await store.deleteAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ // Verify entire directory was removed
+ expect(
+ await fs.promises
+ .access(assetDir)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(false);
+
+ await assertAssetNotExists(store, testData.userId, testData.assetId);
+ });
+
+ it("should remove entire user directory when deleting all user assets", async () => {
+ const userId = "test-user";
+ const testData1 = createTestAssetData({ userId });
+ const testData2 = createTestAssetData({ userId });
+
+ await Promise.all([
+ store.saveAsset({
+ userId: testData1.userId,
+ assetId: testData1.assetId,
+ asset: testData1.content,
+ metadata: testData1.metadata,
+ }),
+ store.saveAsset({
+ userId: testData2.userId,
+ assetId: testData2.assetId,
+ asset: testData2.content,
+ metadata: testData2.metadata,
+ }),
+ ]);
+
+ const userDir = path.join(tempDir, userId);
+
+ // Verify user directory exists
+ expect(
+ await fs.promises
+ .access(userDir)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(true);
+
+ await store.deleteUserAssets({ userId });
+
+ // Verify entire user directory was removed
+ expect(
+ await fs.promises
+ .access(userDir)
+ .then(() => true)
+ .catch(() => false),
+ ).toBe(false);
+ });
+ });
+
+ describe("File System Permissions", () => {
+ it("should create directories with appropriate permissions", async () => {
+ const testData = createTestAssetData();
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const userDir = path.join(tempDir, testData.userId);
+ const assetDir = path.join(userDir, testData.assetId);
+
+ // Verify directories are readable and writable
+ const userStats = await fs.promises.stat(userDir);
+ const assetStats = await fs.promises.stat(assetDir);
+
+ expect(userStats.isDirectory()).toBe(true);
+ expect(assetStats.isDirectory()).toBe(true);
+
+ // Verify we can read and write to the directories
+ await fs.promises.access(userDir, fs.constants.R_OK | fs.constants.W_OK);
+ await fs.promises.access(assetDir, fs.constants.R_OK | fs.constants.W_OK);
+ });
+ });
+});
diff --git a/packages/e2e_tests/tests/assetdb/s3-store.test.ts b/packages/e2e_tests/tests/assetdb/s3-store.test.ts
new file mode 100644
index 00000000..c573750e
--- /dev/null
+++ b/packages/e2e_tests/tests/assetdb/s3-store.test.ts
@@ -0,0 +1,197 @@
+import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+
+import { S3AssetStore } from "@karakeep/shared/assetdb";
+
+import {
+ assertAssetExists,
+ cleanupTestBucket,
+ createS3Store,
+ createTestAssetData,
+ createTestBucket,
+} from "./assetdb-utils";
+
+describe("S3AssetStore - S3-Specific Behaviors", () => {
+ let bucketName: string;
+ let s3Client: S3Client;
+ let store: S3AssetStore;
+
+ beforeEach(async () => {
+ bucketName = `test-bucket-${Math.random().toString(36).substring(7)}`;
+ s3Client = await createTestBucket(bucketName);
+ store = createS3Store(bucketName);
+ });
+
+ afterEach(async () => {
+ await cleanupTestBucket(s3Client, bucketName);
+ });
+
+ describe("S3 Object Structure", () => {
+ it("should store asset as single object with user-defined metadata", async () => {
+ const testData = createTestAssetData();
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ // Verify the object exists with the expected key structure
+ const objectKey = `${testData.userId}/${testData.assetId}`;
+ const headResponse = await s3Client.send(
+ new HeadObjectCommand({
+ Bucket: bucketName,
+ Key: objectKey,
+ }),
+ );
+
+ // Verify metadata is stored in S3 user-defined metadata
+ expect(headResponse.Metadata).toBeDefined();
+ expect(headResponse.Metadata!["x-amz-meta-content-type"]).toBe(
+ testData.metadata.contentType,
+ );
+ expect(headResponse.Metadata!["x-amz-meta-file-name"]).toBe(
+ testData.metadata.fileName || "",
+ );
+
+ // Verify content type is set correctly on the S3 object
+ expect(headResponse.ContentType).toBe(testData.metadata.contentType);
+ });
+
+ it("should handle null fileName in metadata correctly", async () => {
+ const testData = createTestAssetData({
+ metadata: {
+ contentType: "text/html",
+ fileName: null,
+ },
+ });
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ const objectKey = `${testData.userId}/${testData.assetId}`;
+ const headResponse = await s3Client.send(
+ new HeadObjectCommand({
+ Bucket: bucketName,
+ Key: objectKey,
+ }),
+ );
+
+ // Verify null fileName are not stored in S3 metadata
+ expect(headResponse.Metadata!["x-amz-meta-file-name"]).toBeUndefined();
+
+ // Verify reading back gives us null fileName
+ const metadata = await store.readAssetMetadata({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+ expect(metadata.fileName).toBeNull();
+ });
+ });
+
+ describe("S3 Key Structure", () => {
+ it("should use clean userId/assetId key structure", async () => {
+ const userId = "user123";
+ const assetId = "asset456";
+ const testData = createTestAssetData({ userId, assetId });
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ // Verify the exact key structure
+ const expectedKey = `${userId}/${assetId}`;
+ const headResponse = await s3Client.send(
+ new HeadObjectCommand({
+ Bucket: bucketName,
+ Key: expectedKey,
+ }),
+ );
+
+ expect(headResponse.ContentLength).toBe(testData.content.length);
+ });
+
+ it("should handle special characters in user and asset IDs", async () => {
+ const userId = "user/with/slashes";
+ const assetId = "asset-with-special-chars_123";
+ const testData = createTestAssetData({ userId, assetId });
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ await assertAssetExists(store, testData.userId, testData.assetId);
+ });
+ });
+
+ describe("S3 Eventual Consistency", () => {
+ it("should handle immediate read after write (MinIO strong consistency)", async () => {
+ const testData = createTestAssetData();
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ // Immediately try to read - should work with MinIO's strong consistency
+ const { asset, metadata } = await store.readAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(asset).toEqual(testData.content);
+ expect(metadata).toEqual(testData.metadata);
+ });
+ });
+
+ describe("S3 Metadata Conversion", () => {
+ it("should correctly convert between AssetMetadata and S3 metadata", async () => {
+ const testCases = [
+ {
+ contentType: "image/jpeg",
+ fileName: "test-image.jpg",
+ },
+ {
+ contentType: "application/pdf",
+ fileName: "document.pdf",
+ },
+ {
+ contentType: "text/html",
+ fileName: null,
+ },
+ ];
+
+ for (const metadata of testCases) {
+ const testData = createTestAssetData({ metadata });
+
+ await store.saveAsset({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ asset: testData.content,
+ metadata: testData.metadata,
+ });
+
+ // Verify metadata round-trip
+ const retrievedMetadata = await store.readAssetMetadata({
+ userId: testData.userId,
+ assetId: testData.assetId,
+ });
+
+ expect(retrievedMetadata).toEqual(testData.metadata);
+ }
+ });
+ });
+});
diff --git a/packages/shared/assetdb.ts b/packages/shared/assetdb.ts
index 3ac92067..77050406 100644
--- a/packages/shared/assetdb.ts
+++ b/packages/shared/assetdb.ts
@@ -1,5 +1,16 @@
import * as fs from "fs";
import * as path from "path";
+import { Readable } from "stream";
+import {
+ _Object,
+ DeleteObjectCommand,
+ DeleteObjectsCommand,
+ GetObjectCommand,
+ HeadObjectCommand,
+ ListObjectsV2Command,
+ PutObjectCommand,
+ S3Client,
+} from "@aws-sdk/client-s3";
import { Glob } from "glob";
import { z } from "zod";
@@ -43,19 +54,563 @@ export const SUPPORTED_ASSET_TYPES: Set<string> = new Set<string>([
ASSET_TYPES.VIDEO_MP4,
]);
-function getAssetDir(userId: string, assetId: string) {
- return path.join(ROOT_PATH, userId, assetId);
-}
-
export const zAssetMetadataSchema = z.object({
contentType: z.string(),
fileName: z.string().nullish(),
});
+export type AssetMetadata = z.infer<typeof zAssetMetadataSchema>;
+
+export interface AssetInfo {
+ userId: string;
+ assetId: string;
+ contentType: string;
+ fileName?: string | null;
+ size: number;
+}
+
+export interface AssetStore {
+ saveAsset(params: {
+ userId: string;
+ assetId: string;
+ asset: Buffer;
+ metadata: AssetMetadata;
+ }): Promise<void>;
+
+ saveAssetFromFile(params: {
+ userId: string;
+ assetId: string;
+ assetPath: string;
+ metadata: AssetMetadata;
+ }): Promise<void>;
+
+ readAsset(params: {
+ userId: string;
+ assetId: string;
+ }): Promise<{ asset: Buffer; metadata: AssetMetadata }>;
+
+ createAssetReadStream(params: {
+ userId: string;
+ assetId: string;
+ start?: number;
+ end?: number;
+ }): Promise<NodeJS.ReadableStream>;
+
+ readAssetMetadata(params: {
+ userId: string;
+ assetId: string;
+ }): Promise<AssetMetadata>;
+
+ getAssetSize(params: { userId: string; assetId: string }): Promise<number>;
+
+ deleteAsset(params: { userId: string; assetId: string }): Promise<void>;
+
+ deleteUserAssets(params: { userId: string }): Promise<void>;
+
+ getAllAssets(): AsyncGenerator<AssetInfo>;
+}
+
export function newAssetId() {
return crypto.randomUUID();
}
+class LocalFileSystemAssetStore implements AssetStore {
+ private rootPath: string;
+
+ constructor(rootPath: string) {
+ this.rootPath = rootPath;
+ }
+
+ private getAssetDir(userId: string, assetId: string) {
+ return path.join(this.rootPath, userId, assetId);
+ }
+
+ private async isPathExists(filePath: string) {
+ return fs.promises
+ .access(filePath)
+ .then(() => true)
+ .catch(() => false);
+ }
+
+ async saveAsset({
+ userId,
+ assetId,
+ asset,
+ metadata,
+ }: {
+ userId: string;
+ assetId: string;
+ asset: Buffer;
+ metadata: AssetMetadata;
+ }) {
+ if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {
+ throw new Error("Unsupported asset type");
+ }
+ const assetDir = this.getAssetDir(userId, assetId);
+ await fs.promises.mkdir(assetDir, { recursive: true });
+
+ await Promise.all([
+ fs.promises.writeFile(
+ path.join(assetDir, "asset.bin"),
+ Uint8Array.from(asset),
+ ),
+ fs.promises.writeFile(
+ path.join(assetDir, "metadata.json"),
+ JSON.stringify(metadata),
+ ),
+ ]);
+ }
+
+ async saveAssetFromFile({
+ userId,
+ assetId,
+ assetPath,
+ metadata,
+ }: {
+ userId: string;
+ assetId: string;
+ assetPath: string;
+ metadata: AssetMetadata;
+ }) {
+ if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {
+ throw new Error("Unsupported asset type");
+ }
+ const assetDir = this.getAssetDir(userId, assetId);
+ await fs.promises.mkdir(assetDir, { recursive: true });
+
+ await Promise.all([
+ fs.promises.copyFile(assetPath, path.join(assetDir, "asset.bin")),
+ fs.promises.writeFile(
+ path.join(assetDir, "metadata.json"),
+ JSON.stringify(metadata),
+ ),
+ ]);
+ await fs.promises.rm(assetPath);
+ }
+
+ async readAsset({ userId, assetId }: { userId: string; assetId: string }) {
+ const assetDir = this.getAssetDir(userId, assetId);
+
+ const [asset, metadataStr] = await Promise.all([
+ fs.promises.readFile(path.join(assetDir, "asset.bin")),
+ fs.promises.readFile(path.join(assetDir, "metadata.json"), {
+ encoding: "utf8",
+ }),
+ ]);
+
+ const metadata = zAssetMetadataSchema.parse(JSON.parse(metadataStr));
+ return { asset, metadata };
+ }
+
+ async createAssetReadStream({
+ userId,
+ assetId,
+ start,
+ end,
+ }: {
+ userId: string;
+ assetId: string;
+ start?: number;
+ end?: number;
+ }) {
+ const assetDir = this.getAssetDir(userId, assetId);
+ const assetPath = path.join(assetDir, "asset.bin");
+ if (!(await this.isPathExists(assetPath))) {
+ throw new Error(`Asset ${assetId} not found`);
+ }
+
+ return fs.createReadStream(path.join(assetDir, "asset.bin"), {
+ start,
+ end,
+ });
+ }
+
+ async readAssetMetadata({
+ userId,
+ assetId,
+ }: {
+ userId: string;
+ assetId: string;
+ }) {
+ const assetDir = this.getAssetDir(userId, assetId);
+
+ const metadataStr = await fs.promises.readFile(
+ path.join(assetDir, "metadata.json"),
+ {
+ encoding: "utf8",
+ },
+ );
+
+ return zAssetMetadataSchema.parse(JSON.parse(metadataStr));
+ }
+
+ async getAssetSize({ userId, assetId }: { userId: string; assetId: string }) {
+ const assetDir = this.getAssetDir(userId, assetId);
+ const stat = await fs.promises.stat(path.join(assetDir, "asset.bin"));
+ return stat.size;
+ }
+
+ async deleteAsset({ userId, assetId }: { userId: string; assetId: string }) {
+ const assetDir = this.getAssetDir(userId, assetId);
+ if (!(await this.isPathExists(assetDir))) {
+ return;
+ }
+ await fs.promises.rm(assetDir, { recursive: true });
+ }
+
+ async deleteUserAssets({ userId }: { userId: string }) {
+ const userDir = path.join(this.rootPath, userId);
+ const dirExists = await this.isPathExists(userDir);
+ if (!dirExists) {
+ return;
+ }
+ await fs.promises.rm(userDir, { recursive: true });
+ }
+
+ async *getAllAssets() {
+ const g = new Glob(`/**/**/asset.bin`, {
+ maxDepth: 3,
+ root: this.rootPath,
+ cwd: this.rootPath,
+ absolute: false,
+ });
+ for await (const file of g) {
+ const [userId, assetId] = file.split("/").slice(0, 2);
+ const [size, metadata] = await Promise.all([
+ this.getAssetSize({ userId, assetId }),
+ this.readAssetMetadata({ userId, assetId }),
+ ]);
+ yield {
+ userId,
+ assetId,
+ ...metadata,
+ size,
+ };
+ }
+ }
+}
+
+class S3AssetStore implements AssetStore {
+ private s3Client: S3Client;
+ private bucketName: string;
+
+ constructor(s3Client: S3Client, bucketName: string) {
+ this.s3Client = s3Client;
+ this.bucketName = bucketName;
+ }
+
+ private getAssetKey(userId: string, assetId: string) {
+ return `${userId}/${assetId}`;
+ }
+
+ private metadataToS3Metadata(
+ metadata: AssetMetadata,
+ ): Record<string, string> {
+ return {
+ ...(metadata.fileName
+ ? { "x-amz-meta-file-name": metadata.fileName }
+ : {}),
+ "x-amz-meta-content-type": metadata.contentType,
+ };
+ }
+
+ private s3MetadataToMetadata(
+ s3Metadata: Record<string, string> | undefined,
+ ): AssetMetadata {
+ if (!s3Metadata) {
+ throw new Error("No metadata found in S3 object");
+ }
+
+ return {
+ contentType: s3Metadata["x-amz-meta-content-type"] || "",
+ fileName: s3Metadata["x-amz-meta-file-name"] ?? null,
+ };
+ }
+
+ async saveAsset({
+ userId,
+ assetId,
+ asset,
+ metadata,
+ }: {
+ userId: string;
+ assetId: string;
+ asset: Buffer;
+ metadata: AssetMetadata;
+ }) {
+ if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {
+ throw new Error("Unsupported asset type");
+ }
+
+ await this.s3Client.send(
+ new PutObjectCommand({
+ Bucket: this.bucketName,
+ Key: this.getAssetKey(userId, assetId),
+ Body: asset,
+ ContentType: metadata.contentType,
+ Metadata: this.metadataToS3Metadata(metadata),
+ }),
+ );
+ }
+
+ async saveAssetFromFile({
+ userId,
+ assetId,
+ assetPath,
+ metadata,
+ }: {
+ userId: string;
+ assetId: string;
+ assetPath: string;
+ metadata: AssetMetadata;
+ }) {
+ if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {
+ throw new Error("Unsupported asset type");
+ }
+
+ const asset = fs.createReadStream(assetPath);
+ await this.s3Client.send(
+ new PutObjectCommand({
+ Bucket: this.bucketName,
+ Key: this.getAssetKey(userId, assetId),
+ Body: asset,
+ ContentType: metadata.contentType,
+ Metadata: this.metadataToS3Metadata(metadata),
+ }),
+ );
+ await fs.promises.rm(assetPath);
+ }
+
+ async readAsset({ userId, assetId }: { userId: string; assetId: string }) {
+ const response = await this.s3Client.send(
+ new GetObjectCommand({
+ Bucket: this.bucketName,
+ Key: this.getAssetKey(userId, assetId),
+ }),
+ );
+
+ if (!response.Body) {
+ throw new Error("Asset not found");
+ }
+
+ const assetBuffer = await this.streamToBuffer(response.Body as Readable);
+ const metadata = this.s3MetadataToMetadata(response.Metadata);
+
+ return { asset: assetBuffer, metadata };
+ }
+
+ async createAssetReadStream({
+ userId,
+ assetId,
+ start,
+ end,
+ }: {
+ userId: string;
+ assetId: string;
+ start?: number;
+ end?: number;
+ }) {
+ const range =
+ start !== undefined && end !== undefined
+ ? `bytes=${start}-${end}`
+ : undefined;
+
+ const command = new GetObjectCommand({
+ Bucket: this.bucketName,
+ Key: this.getAssetKey(userId, assetId),
+ Range: range,
+ });
+
+ const response = await this.s3Client.send(command);
+ if (!response.Body) {
+ throw new Error("Asset not found");
+ }
+ return response.Body as NodeJS.ReadableStream;
+ }
+
+ async readAssetMetadata({
+ userId,
+ assetId,
+ }: {
+ userId: string;
+ assetId: string;
+ }) {
+ const response = await this.s3Client.send(
+ new HeadObjectCommand({
+ Bucket: this.bucketName,
+ Key: this.getAssetKey(userId, assetId),
+ }),
+ );
+
+ return this.s3MetadataToMetadata(response.Metadata);
+ }
+
+ async getAssetSize({ userId, assetId }: { userId: string; assetId: string }) {
+ const response = await this.s3Client.send(
+ new HeadObjectCommand({
+ Bucket: this.bucketName,
+ Key: this.getAssetKey(userId, assetId),
+ }),
+ );
+
+ return response.ContentLength || 0;
+ }
+
+ async deleteAsset({ userId, assetId }: { userId: string; assetId: string }) {
+ await this.s3Client.send(
+ new DeleteObjectCommand({
+ Bucket: this.bucketName,
+ Key: this.getAssetKey(userId, assetId),
+ }),
+ );
+ }
+
+ async deleteUserAssets({ userId }: { userId: string }) {
+ let continuationToken: string | undefined;
+
+ do {
+ const listResponse = await this.s3Client.send(
+ new ListObjectsV2Command({
+ Bucket: this.bucketName,
+ Prefix: `${userId}/`,
+ ContinuationToken: continuationToken,
+ }),
+ );
+
+ if (listResponse.Contents && listResponse.Contents.length > 0) {
+ await this.s3Client.send(
+ new DeleteObjectsCommand({
+ Bucket: this.bucketName,
+ Delete: {
+ Objects: listResponse.Contents.map((obj) => ({
+ Key: obj.Key!,
+ })),
+ },
+ }),
+ );
+ }
+
+ continuationToken = listResponse.NextContinuationToken;
+ } while (continuationToken);
+ }
+
+ async *getAllAssets() {
+ let continuationToken: string | undefined;
+
+ do {
+ const listResponse = await this.s3Client.send(
+ new ListObjectsV2Command({
+ Bucket: this.bucketName,
+ ContinuationToken: continuationToken,
+ }),
+ );
+
+ if (listResponse.Contents) {
+ for (const obj of listResponse.Contents) {
+ if (!obj.Key) continue;
+
+ const pathParts = obj.Key.split("/");
+ if (pathParts.length === 2) {
+ const userId = pathParts[0];
+ const assetId = pathParts[1];
+
+ try {
+ const headResponse = await this.s3Client.send(
+ new HeadObjectCommand({
+ Bucket: this.bucketName,
+ Key: obj.Key,
+ }),
+ );
+
+ const metadata = this.s3MetadataToMetadata(headResponse.Metadata);
+ const size = headResponse.ContentLength || 0;
+
+ yield {
+ userId,
+ assetId,
+ ...metadata,
+ size,
+ };
+ } catch (error) {
+ logger.warn(`Failed to read asset ${userId}/${assetId}:`, error);
+ }
+ }
+ }
+ }
+
+ continuationToken = listResponse.NextContinuationToken;
+ } while (continuationToken);
+ }
+
+ private async streamToBuffer(stream: Readable): Promise<Buffer> {
+ const chunks: Buffer[] = [];
+ for await (const chunk of stream) {
+ chunks.push(chunk);
+ }
+ return Buffer.concat(chunks);
+ }
+}
+
+function createDefaultAssetStore(): AssetStore {
+ const config = serverConfig.assetStore;
+
+ if (config.type === "s3") {
+ if (!config.s3.bucket) {
+ throw new Error(
+ "ASSET_STORE_S3_BUCKET is required when using S3 asset store",
+ );
+ }
+ if (!config.s3.accessKeyId || !config.s3.secretAccessKey) {
+ throw new Error(
+ "ASSET_STORE_S3_ACCESS_KEY_ID and ASSET_STORE_S3_SECRET_ACCESS_KEY are required when using S3 asset store",
+ );
+ }
+
+ const s3Client = new S3Client({
+ region: config.s3.region,
+ endpoint: config.s3.endpoint,
+ forcePathStyle: config.s3.forcePathStyle,
+ credentials: {
+ accessKeyId: config.s3.accessKeyId,
+ secretAccessKey: config.s3.secretAccessKey,
+ },
+ });
+
+ return new S3AssetStore(s3Client, config.s3.bucket);
+ }
+
+ return new LocalFileSystemAssetStore(ROOT_PATH);
+}
+
+const defaultAssetStore = createDefaultAssetStore();
+
+export { LocalFileSystemAssetStore, S3AssetStore };
+
+/**
+ * Example usage of S3AssetStore:
+ *
+ * import { S3Client } from "@aws-sdk/client-s3";
+ * import { S3AssetStore } from "@karakeep/shared/assetdb";
+ *
+ * const s3Client = new S3Client({
+ * region: "us-east-1",
+ * credentials: {
+ * accessKeyId: "your-access-key",
+ * secretAccessKey: "your-secret-key"
+ * }
+ * });
+ *
+ * const s3AssetStore = new S3AssetStore(s3Client, "your-bucket-name");
+ *
+ * // Use s3AssetStore instead of the default file system store
+ * await s3AssetStore.saveAsset({
+ * userId: "user123",
+ * assetId: "asset456",
+ * asset: buffer,
+ * metadata: { contentType: "image/jpeg", fileName: "photo.jpg" }
+ * });
+ */
+
export async function saveAsset({
userId,
assetId,
@@ -67,22 +622,7 @@ export async function saveAsset({
asset: Buffer;
metadata: z.infer<typeof zAssetMetadataSchema>;
}) {
- if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {
- throw new Error("Unsupported asset type");
- }
- const assetDir = getAssetDir(userId, assetId);
- await fs.promises.mkdir(assetDir, { recursive: true });
-
- await Promise.all([
- fs.promises.writeFile(
- path.join(assetDir, "asset.bin"),
- Uint8Array.from(asset),
- ),
- fs.promises.writeFile(
- path.join(assetDir, "metadata.json"),
- JSON.stringify(metadata),
- ),
- ]);
+ return defaultAssetStore.saveAsset({ userId, assetId, asset, metadata });
}
export async function saveAssetFromFile({
@@ -96,22 +636,12 @@ export async function saveAssetFromFile({
assetPath: string;
metadata: z.infer<typeof zAssetMetadataSchema>;
}) {
- if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {
- throw new Error("Unsupported asset type");
- }
- const assetDir = getAssetDir(userId, assetId);
- await fs.promises.mkdir(assetDir, { recursive: true });
-
- await Promise.all([
- // We'll have to copy first then delete the original file as inside the docker container
- // we can't move file between mounts.
- fs.promises.copyFile(assetPath, path.join(assetDir, "asset.bin")),
- fs.promises.writeFile(
- path.join(assetDir, "metadata.json"),
- JSON.stringify(metadata),
- ),
- ]);
- await fs.promises.rm(assetPath);
+ return defaultAssetStore.saveAssetFromFile({
+ userId,
+ assetId,
+ assetPath,
+ metadata,
+ });
}
export async function readAsset({
@@ -121,20 +651,10 @@ export async function readAsset({
userId: string;
assetId: string;
}) {
- const assetDir = getAssetDir(userId, assetId);
-
- const [asset, metadataStr] = await Promise.all([
- fs.promises.readFile(path.join(assetDir, "asset.bin")),
- fs.promises.readFile(path.join(assetDir, "metadata.json"), {
- encoding: "utf8",
- }),
- ]);
-
- const metadata = zAssetMetadataSchema.parse(JSON.parse(metadataStr));
- return { asset, metadata };
+ return defaultAssetStore.readAsset({ userId, assetId });
}
-export function createAssetReadStream({
+export async function createAssetReadStream({
userId,
assetId,
start,
@@ -145,9 +665,9 @@ export function createAssetReadStream({
start?: number;
end?: number;
}) {
- const assetDir = getAssetDir(userId, assetId);
-
- return fs.createReadStream(path.join(assetDir, "asset.bin"), {
+ return defaultAssetStore.createAssetReadStream({
+ userId,
+ assetId,
start,
end,
});
@@ -160,16 +680,7 @@ export async function readAssetMetadata({
userId: string;
assetId: string;
}) {
- const assetDir = getAssetDir(userId, assetId);
-
- const metadataStr = await fs.promises.readFile(
- path.join(assetDir, "metadata.json"),
- {
- encoding: "utf8",
- },
- );
-
- return zAssetMetadataSchema.parse(JSON.parse(metadataStr));
+ return defaultAssetStore.readAssetMetadata({ userId, assetId });
}
export async function getAssetSize({
@@ -179,9 +690,7 @@ export async function getAssetSize({
userId: string;
assetId: string;
}) {
- const assetDir = getAssetDir(userId, assetId);
- const stat = await fs.promises.stat(path.join(assetDir, "asset.bin"));
- return stat.size;
+ return defaultAssetStore.getAssetSize({ userId, assetId });
}
/**
@@ -205,42 +714,15 @@ export async function deleteAsset({
userId: string;
assetId: string;
}) {
- const assetDir = getAssetDir(userId, assetId);
- await fs.promises.rm(path.join(assetDir), { recursive: true });
+ return defaultAssetStore.deleteAsset({ userId, assetId });
}
export async function deleteUserAssets({ userId }: { userId: string }) {
- const userDir = path.join(ROOT_PATH, userId);
- const dirExists = await fs.promises
- .access(userDir)
- .then(() => true)
- .catch(() => false);
- if (!dirExists) {
- return;
- }
- await fs.promises.rm(userDir, { recursive: true });
+ return defaultAssetStore.deleteUserAssets({ userId });
}
export async function* getAllAssets() {
- const g = new Glob(`/**/**/asset.bin`, {
- maxDepth: 3,
- root: ROOT_PATH,
- cwd: ROOT_PATH,
- absolute: false,
- });
- for await (const file of g) {
- const [userId, assetId] = file.split("/").slice(0, 2);
- const [size, metadata] = await Promise.all([
- getAssetSize({ userId, assetId }),
- readAssetMetadata({ userId, assetId }),
- ]);
- yield {
- userId,
- assetId,
- ...metadata,
- size,
- };
- }
+ yield* defaultAssetStore.getAllAssets();
}
export async function storeScreenshot(
diff --git a/packages/shared/config.ts b/packages/shared/config.ts
index b899dbeb..715c2848 100644
--- a/packages/shared/config.ts
+++ b/packages/shared/config.ts
@@ -88,6 +88,14 @@ const allEnv = z.object({
// A flag to detect if the user is running in the old separete containers setup
USING_LEGACY_SEPARATE_CONTAINERS: stringBool("false"),
+
+ // Asset storage configuration
+ ASSET_STORE_S3_ENDPOINT: z.string().optional(),
+ ASSET_STORE_S3_REGION: z.string().optional(),
+ ASSET_STORE_S3_BUCKET: z.string().optional(),
+ ASSET_STORE_S3_ACCESS_KEY_ID: z.string().optional(),
+ ASSET_STORE_S3_SECRET_ACCESS_KEY: z.string().optional(),
+ ASSET_STORE_S3_FORCE_PATH_STYLE: stringBool("false"),
});
const serverConfigSchema = allEnv.transform((val) => {
@@ -185,6 +193,19 @@ const serverConfigSchema = allEnv.transform((val) => {
timeoutSec: val.WEBHOOK_TIMEOUT_SEC,
retryTimes: val.WEBHOOK_RETRY_TIMES,
},
+ assetStore: {
+ type: val.ASSET_STORE_S3_ENDPOINT
+ ? ("s3" as const)
+ : ("filesystem" as const),
+ s3: {
+ endpoint: val.ASSET_STORE_S3_ENDPOINT,
+ region: val.ASSET_STORE_S3_REGION,
+ bucket: val.ASSET_STORE_S3_BUCKET,
+ accessKeyId: val.ASSET_STORE_S3_ACCESS_KEY_ID,
+ secretAccessKey: val.ASSET_STORE_S3_SECRET_ACCESS_KEY,
+ forcePathStyle: val.ASSET_STORE_S3_FORCE_PATH_STYLE,
+ },
+ },
};
});
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 691e1d25..f4e521b6 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -5,6 +5,7 @@
"private": true,
"type": "module",
"dependencies": {
+ "@aws-sdk/client-s3": "^3.842.0",
"glob": "^11.0.0",
"js-tiktoken": "^1.0.20",
"liteque": "^0.3.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 56038d5b..cafcae03 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1018,9 +1018,15 @@ importers:
packages/e2e_tests:
dependencies:
+ '@aws-sdk/client-s3':
+ specifier: ^3.842.0
+ version: 3.842.0
'@karakeep/sdk':
specifier: workspace:*
version: link:../sdk
+ '@karakeep/shared':
+ specifier: workspace:^0.1.0
+ version: link:../shared
'@karakeep/trpc':
specifier: workspace:^0.1.0
version: link:../trpc
@@ -1093,6 +1099,9 @@ importers:
packages/shared:
dependencies:
+ '@aws-sdk/client-s3':
+ specifier: ^3.842.0
+ version: 3.842.0
glob:
specifier: ^11.0.0
version: 11.0.2
@@ -1416,6 +1425,157 @@ packages:
'@auth/drizzle-adapter@1.5.3':
resolution: {integrity: sha512-VNyYb1hiGtorJhCjShtncjN3TKXxtwxOwphYecq8lZSVuFDLIWHhp4ZbdZDjnmkvEk8G66IpFrYW84qpt+WUIg==}
+ '@aws-crypto/crc32@5.2.0':
+ resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
+ engines: {node: '>=16.0.0'}
+
+ '@aws-crypto/crc32c@5.2.0':
+ resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==}
+
+ '@aws-crypto/sha1-browser@5.2.0':
+ resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
+
+ '@aws-crypto/sha256-browser@5.2.0':
+ resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
+
+ '@aws-crypto/sha256-js@5.2.0':
+ resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==}
+ engines: {node: '>=16.0.0'}
+
+ '@aws-crypto/supports-web-crypto@5.2.0':
+ resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==}
+
+ '@aws-crypto/util@5.2.0':
+ resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
+
+ '@aws-sdk/client-s3@3.842.0':
+ resolution: {integrity: sha512-T5Rh72Rcq1xIaM8KkTr1Wpr7/WPCYO++KrM+/Em0rq2jxpjMMhj77ITpgH7eEmNxWmwIndTwqpgfmbpNfk7Gbw==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/client-sso@3.840.0':
+ resolution: {integrity: sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/core@3.840.0':
+ resolution: {integrity: sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/credential-provider-env@3.840.0':
+ resolution: {integrity: sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/credential-provider-http@3.840.0':
+ resolution: {integrity: sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/credential-provider-ini@3.840.0':
+ resolution: {integrity: sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/credential-provider-node@3.840.0':
+ resolution: {integrity: sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/credential-provider-process@3.840.0':
+ resolution: {integrity: sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/credential-provider-sso@3.840.0':
+ resolution: {integrity: sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/credential-provider-web-identity@3.840.0':
+ resolution: {integrity: sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-bucket-endpoint@3.840.0':
+ resolution: {integrity: sha512-+gkQNtPwcSMmlwBHFd4saVVS11In6ID1HczNzpM3MXKXRBfSlbZJbCt6wN//AZ8HMklZEik4tcEOG0qa9UY8SQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-expect-continue@3.840.0':
+ resolution: {integrity: sha512-iJg2r6FKsKKvdiU4oCOuCf7Ro/YE0Q2BT/QyEZN3/Rt8Nr4SAZiQOlcBXOCpGvuIKOEAhvDOUnW3aDHL01PdVw==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-flexible-checksums@3.840.0':
+ resolution: {integrity: sha512-Kg/o2G6o72sdoRH0J+avdcf668gM1bp6O4VeEXpXwUj/urQnV5qiB2q1EYT110INHUKWOLXPND3sQAqh6sTqHw==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-host-header@3.840.0':
+ resolution: {integrity: sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-location-constraint@3.840.0':
+ resolution: {integrity: sha512-KVLD0u0YMF3aQkVF8bdyHAGWSUY6N1Du89htTLgqCcIhSxxAJ9qifrosVZ9jkAzqRW99hcufyt2LylcVU2yoKQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-logger@3.840.0':
+ resolution: {integrity: sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-recursion-detection@3.840.0':
+ resolution: {integrity: sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-sdk-s3@3.840.0':
+ resolution: {integrity: sha512-rOUji7CayWN3O09zvvgLzDVQe0HiJdZkxoTS6vzOS3WbbdT7joGdVtAJHtn+x776QT3hHzbKU5gnfhel0o6gQA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-ssec@3.840.0':
+ resolution: {integrity: sha512-CBZP9t1QbjDFGOrtnUEHL1oAvmnCUUm7p0aPNbIdSzNtH42TNKjPRN3TuEIJDGjkrqpL3MXyDSmNayDcw/XW7Q==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/middleware-user-agent@3.840.0':
+ resolution: {integrity: sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/nested-clients@3.840.0':
+ resolution: {integrity: sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/region-config-resolver@3.840.0':
+ resolution: {integrity: sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/signature-v4-multi-region@3.840.0':
+ resolution: {integrity: sha512-8AoVgHrkSfhvGPtwx23hIUO4MmMnux2pjnso1lrLZGqxfElM6jm2w4jTNLlNXk8uKHGyX89HaAIuT0lL6dJj9g==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/token-providers@3.840.0':
+ resolution: {integrity: sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/types@3.840.0':
+ resolution: {integrity: sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/util-arn-parser@3.804.0':
+ resolution: {integrity: sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/util-endpoints@3.840.0':
+ resolution: {integrity: sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/util-locate-window@3.804.0':
+ resolution: {integrity: sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==}
+ engines: {node: '>=18.0.0'}
+
+ '@aws-sdk/util-user-agent-browser@3.840.0':
+ resolution: {integrity: sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==}
+
+ '@aws-sdk/util-user-agent-node@3.840.0':
+ resolution: {integrity: sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ aws-crt: '>=1.0.0'
+ peerDependenciesMeta:
+ aws-crt:
+ optional: true
+
+ '@aws-sdk/xml-builder@3.821.0':
+ resolution: {integrity: sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==}
+ engines: {node: '>=18.0.0'}
+
'@babel/code-frame@7.10.4':
resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==}
@@ -4644,6 +4804,218 @@ packages:
'@slorber/remark-comment@1.0.0':
resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==}
+ '@smithy/abort-controller@4.0.4':
+ resolution: {integrity: sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/chunked-blob-reader-native@4.0.0':
+ resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/chunked-blob-reader@5.0.0':
+ resolution: {integrity: sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/config-resolver@4.1.4':
+ resolution: {integrity: sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/core@3.6.0':
+ resolution: {integrity: sha512-Pgvfb+TQ4wUNLyHzvgCP4aYZMh16y7GcfF59oirRHcgGgkH1e/s9C0nv/v3WP+Quymyr5je71HeFQCwh+44XLg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/credential-provider-imds@4.0.6':
+ resolution: {integrity: sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-codec@4.0.4':
+ resolution: {integrity: sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-browser@4.0.4':
+ resolution: {integrity: sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-config-resolver@4.1.2':
+ resolution: {integrity: sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-node@4.0.4':
+ resolution: {integrity: sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-universal@4.0.4':
+ resolution: {integrity: sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/fetch-http-handler@5.0.4':
+ resolution: {integrity: sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/hash-blob-browser@4.0.4':
+ resolution: {integrity: sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/hash-node@4.0.4':
+ resolution: {integrity: sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/hash-stream-node@4.0.4':
+ resolution: {integrity: sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/invalid-dependency@4.0.4':
+ resolution: {integrity: sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/is-array-buffer@2.2.0':
+ resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==}
+ engines: {node: '>=14.0.0'}
+
+ '@smithy/is-array-buffer@4.0.0':
+ resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/md5-js@4.0.4':
+ resolution: {integrity: sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-content-length@4.0.4':
+ resolution: {integrity: sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-endpoint@4.1.13':
+ resolution: {integrity: sha512-xg3EHV/Q5ZdAO5b0UiIMj3RIOCobuS40pBBODguUDVdko6YK6QIzCVRrHTogVuEKglBWqWenRnZ71iZnLL3ZAQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-retry@4.1.14':
+ resolution: {integrity: sha512-eoXaLlDGpKvdmvt+YBfRXE7HmIEtFF+DJCbTPwuLunP0YUnrydl+C4tS+vEM0+nyxXrX3PSUFqC+lP1+EHB1Tw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-serde@4.0.8':
+ resolution: {integrity: sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-stack@4.0.4':
+ resolution: {integrity: sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/node-config-provider@4.1.3':
+ resolution: {integrity: sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/node-http-handler@4.0.6':
+ resolution: {integrity: sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/property-provider@4.0.4':
+ resolution: {integrity: sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/protocol-http@5.1.2':
+ resolution: {integrity: sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/querystring-builder@4.0.4':
+ resolution: {integrity: sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/querystring-parser@4.0.4':
+ resolution: {integrity: sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/service-error-classification@4.0.6':
+ resolution: {integrity: sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/shared-ini-file-loader@4.0.4':
+ resolution: {integrity: sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/signature-v4@5.1.2':
+ resolution: {integrity: sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/smithy-client@4.4.5':
+ resolution: {integrity: sha512-+lynZjGuUFJaMdDYSTMnP/uPBBXXukVfrJlP+1U/Dp5SFTEI++w6NMga8DjOENxecOF71V9Z2DllaVDYRnGlkg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/types@4.3.1':
+ resolution: {integrity: sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/url-parser@4.0.4':
+ resolution: {integrity: sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-base64@4.0.0':
+ resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-body-length-browser@4.0.0':
+ resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-body-length-node@4.0.0':
+ resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-buffer-from@2.2.0':
+ resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
+ engines: {node: '>=14.0.0'}
+
+ '@smithy/util-buffer-from@4.0.0':
+ resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-config-provider@4.0.0':
+ resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-defaults-mode-browser@4.0.21':
+ resolution: {integrity: sha512-wM0jhTytgXu3wzJoIqpbBAG5U6BwiubZ6QKzSbP7/VbmF1v96xlAbX2Am/mz0Zep0NLvLh84JT0tuZnk3wmYQA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-defaults-mode-node@4.0.21':
+ resolution: {integrity: sha512-/F34zkoU0GzpUgLJydHY8Rxu9lBn8xQC/s/0M0U9lLBkYbA1htaAFjWYJzpzsbXPuri5D1H8gjp2jBum05qBrA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-endpoints@3.0.6':
+ resolution: {integrity: sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-hex-encoding@4.0.0':
+ resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-middleware@4.0.4':
+ resolution: {integrity: sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-retry@4.0.6':
+ resolution: {integrity: sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-stream@4.2.2':
+ resolution: {integrity: sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-uri-escape@4.0.0':
+ resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-utf8@2.3.0':
+ resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==}
+ engines: {node: '>=14.0.0'}
+
+ '@smithy/util-utf8@4.0.0':
+ resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-waiter@4.0.6':
+ resolution: {integrity: sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==}
+ engines: {node: '>=18.0.0'}
+
'@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
@@ -5159,6 +5531,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+ '@types/uuid@9.0.8':
+ resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
+
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@@ -5737,6 +6112,9 @@ packages:
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+ bowser@2.11.0:
+ resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==}
+
boxen@6.2.1:
resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -7554,6 +7932,10 @@ packages:
fast-uri@3.0.6:
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
+ fast-xml-parser@4.4.1:
+ resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
+ hasBin: true
+
fastest-levenshtein@1.0.16:
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
engines: {node: '>= 4.9.1'}
@@ -12739,6 +13121,9 @@ packages:
strip-literal@2.1.1:
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
+ strnum@1.1.2:
+ resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
+
structured-headers@0.4.1:
resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
@@ -13411,6 +13796,10 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
+ uuid@9.0.1:
+ resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
+ hasBin: true
+
uvu@0.5.6:
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==}
engines: {node: '>=8'}
@@ -14181,6 +14570,471 @@ snapshots:
- '@simplewebauthn/server'
- nodemailer
+ '@aws-crypto/crc32@5.2.0':
+ dependencies:
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.840.0
+ tslib: 2.8.1
+
+ '@aws-crypto/crc32c@5.2.0':
+ dependencies:
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.840.0
+ tslib: 2.8.1
+
+ '@aws-crypto/sha1-browser@5.2.0':
+ dependencies:
+ '@aws-crypto/supports-web-crypto': 5.2.0
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-locate-window': 3.804.0
+ '@smithy/util-utf8': 2.3.0
+ tslib: 2.8.1
+
+ '@aws-crypto/sha256-browser@5.2.0':
+ dependencies:
+ '@aws-crypto/sha256-js': 5.2.0
+ '@aws-crypto/supports-web-crypto': 5.2.0
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-locate-window': 3.804.0
+ '@smithy/util-utf8': 2.3.0
+ tslib: 2.8.1
+
+ '@aws-crypto/sha256-js@5.2.0':
+ dependencies:
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.840.0
+ tslib: 2.8.1
+
+ '@aws-crypto/supports-web-crypto@5.2.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@aws-crypto/util@5.2.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/util-utf8': 2.3.0
+ tslib: 2.8.1
+
+ '@aws-sdk/client-s3@3.842.0':
+ dependencies:
+ '@aws-crypto/sha1-browser': 5.2.0
+ '@aws-crypto/sha256-browser': 5.2.0
+ '@aws-crypto/sha256-js': 5.2.0
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/credential-provider-node': 3.840.0
+ '@aws-sdk/middleware-bucket-endpoint': 3.840.0
+ '@aws-sdk/middleware-expect-continue': 3.840.0
+ '@aws-sdk/middleware-flexible-checksums': 3.840.0
+ '@aws-sdk/middleware-host-header': 3.840.0
+ '@aws-sdk/middleware-location-constraint': 3.840.0
+ '@aws-sdk/middleware-logger': 3.840.0
+ '@aws-sdk/middleware-recursion-detection': 3.840.0
+ '@aws-sdk/middleware-sdk-s3': 3.840.0
+ '@aws-sdk/middleware-ssec': 3.840.0
+ '@aws-sdk/middleware-user-agent': 3.840.0
+ '@aws-sdk/region-config-resolver': 3.840.0
+ '@aws-sdk/signature-v4-multi-region': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-endpoints': 3.840.0
+ '@aws-sdk/util-user-agent-browser': 3.840.0
+ '@aws-sdk/util-user-agent-node': 3.840.0
+ '@aws-sdk/xml-builder': 3.821.0
+ '@smithy/config-resolver': 4.1.4
+ '@smithy/core': 3.6.0
+ '@smithy/eventstream-serde-browser': 4.0.4
+ '@smithy/eventstream-serde-config-resolver': 4.1.2
+ '@smithy/eventstream-serde-node': 4.0.4
+ '@smithy/fetch-http-handler': 5.0.4
+ '@smithy/hash-blob-browser': 4.0.4
+ '@smithy/hash-node': 4.0.4
+ '@smithy/hash-stream-node': 4.0.4
+ '@smithy/invalid-dependency': 4.0.4
+ '@smithy/md5-js': 4.0.4
+ '@smithy/middleware-content-length': 4.0.4
+ '@smithy/middleware-endpoint': 4.1.13
+ '@smithy/middleware-retry': 4.1.14
+ '@smithy/middleware-serde': 4.0.8
+ '@smithy/middleware-stack': 4.0.4
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/node-http-handler': 4.0.6
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ '@smithy/url-parser': 4.0.4
+ '@smithy/util-base64': 4.0.0
+ '@smithy/util-body-length-browser': 4.0.0
+ '@smithy/util-body-length-node': 4.0.0
+ '@smithy/util-defaults-mode-browser': 4.0.21
+ '@smithy/util-defaults-mode-node': 4.0.21
+ '@smithy/util-endpoints': 3.0.6
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-retry': 4.0.6
+ '@smithy/util-stream': 4.2.2
+ '@smithy/util-utf8': 4.0.0
+ '@smithy/util-waiter': 4.0.6
+ '@types/uuid': 9.0.8
+ tslib: 2.8.1
+ uuid: 9.0.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/client-sso@3.840.0':
+ dependencies:
+ '@aws-crypto/sha256-browser': 5.2.0
+ '@aws-crypto/sha256-js': 5.2.0
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/middleware-host-header': 3.840.0
+ '@aws-sdk/middleware-logger': 3.840.0
+ '@aws-sdk/middleware-recursion-detection': 3.840.0
+ '@aws-sdk/middleware-user-agent': 3.840.0
+ '@aws-sdk/region-config-resolver': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-endpoints': 3.840.0
+ '@aws-sdk/util-user-agent-browser': 3.840.0
+ '@aws-sdk/util-user-agent-node': 3.840.0
+ '@smithy/config-resolver': 4.1.4
+ '@smithy/core': 3.6.0
+ '@smithy/fetch-http-handler': 5.0.4
+ '@smithy/hash-node': 4.0.4
+ '@smithy/invalid-dependency': 4.0.4
+ '@smithy/middleware-content-length': 4.0.4
+ '@smithy/middleware-endpoint': 4.1.13
+ '@smithy/middleware-retry': 4.1.14
+ '@smithy/middleware-serde': 4.0.8
+ '@smithy/middleware-stack': 4.0.4
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/node-http-handler': 4.0.6
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ '@smithy/url-parser': 4.0.4
+ '@smithy/util-base64': 4.0.0
+ '@smithy/util-body-length-browser': 4.0.0
+ '@smithy/util-body-length-node': 4.0.0
+ '@smithy/util-defaults-mode-browser': 4.0.21
+ '@smithy/util-defaults-mode-node': 4.0.21
+ '@smithy/util-endpoints': 3.0.6
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-retry': 4.0.6
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/core@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/xml-builder': 3.821.0
+ '@smithy/core': 3.6.0
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/property-provider': 4.0.4
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/signature-v4': 5.1.2
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ '@smithy/util-base64': 4.0.0
+ '@smithy/util-body-length-browser': 4.0.0
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-utf8': 4.0.0
+ fast-xml-parser: 4.4.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-env@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/property-provider': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-http@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/fetch-http-handler': 5.0.4
+ '@smithy/node-http-handler': 4.0.6
+ '@smithy/property-provider': 4.0.4
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ '@smithy/util-stream': 4.2.2
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-ini@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/credential-provider-env': 3.840.0
+ '@aws-sdk/credential-provider-http': 3.840.0
+ '@aws-sdk/credential-provider-process': 3.840.0
+ '@aws-sdk/credential-provider-sso': 3.840.0
+ '@aws-sdk/credential-provider-web-identity': 3.840.0
+ '@aws-sdk/nested-clients': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/credential-provider-imds': 4.0.6
+ '@smithy/property-provider': 4.0.4
+ '@smithy/shared-ini-file-loader': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/credential-provider-node@3.840.0':
+ dependencies:
+ '@aws-sdk/credential-provider-env': 3.840.0
+ '@aws-sdk/credential-provider-http': 3.840.0
+ '@aws-sdk/credential-provider-ini': 3.840.0
+ '@aws-sdk/credential-provider-process': 3.840.0
+ '@aws-sdk/credential-provider-sso': 3.840.0
+ '@aws-sdk/credential-provider-web-identity': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/credential-provider-imds': 4.0.6
+ '@smithy/property-provider': 4.0.4
+ '@smithy/shared-ini-file-loader': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/credential-provider-process@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/property-provider': 4.0.4
+ '@smithy/shared-ini-file-loader': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-sso@3.840.0':
+ dependencies:
+ '@aws-sdk/client-sso': 3.840.0
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/token-providers': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/property-provider': 4.0.4
+ '@smithy/shared-ini-file-loader': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/credential-provider-web-identity@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/nested-clients': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/property-provider': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/middleware-bucket-endpoint@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-arn-parser': 3.804.0
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ '@smithy/util-config-provider': 4.0.0
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-expect-continue@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-flexible-checksums@3.840.0':
+ dependencies:
+ '@aws-crypto/crc32': 5.2.0
+ '@aws-crypto/crc32c': 5.2.0
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/is-array-buffer': 4.0.0
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-stream': 4.2.2
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-host-header@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-location-constraint@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-logger@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-recursion-detection@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-sdk-s3@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-arn-parser': 3.804.0
+ '@smithy/core': 3.6.0
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/signature-v4': 5.1.2
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ '@smithy/util-config-provider': 4.0.0
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-stream': 4.2.2
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-ssec@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-user-agent@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-endpoints': 3.840.0
+ '@smithy/core': 3.6.0
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/nested-clients@3.840.0':
+ dependencies:
+ '@aws-crypto/sha256-browser': 5.2.0
+ '@aws-crypto/sha256-js': 5.2.0
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/middleware-host-header': 3.840.0
+ '@aws-sdk/middleware-logger': 3.840.0
+ '@aws-sdk/middleware-recursion-detection': 3.840.0
+ '@aws-sdk/middleware-user-agent': 3.840.0
+ '@aws-sdk/region-config-resolver': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@aws-sdk/util-endpoints': 3.840.0
+ '@aws-sdk/util-user-agent-browser': 3.840.0
+ '@aws-sdk/util-user-agent-node': 3.840.0
+ '@smithy/config-resolver': 4.1.4
+ '@smithy/core': 3.6.0
+ '@smithy/fetch-http-handler': 5.0.4
+ '@smithy/hash-node': 4.0.4
+ '@smithy/invalid-dependency': 4.0.4
+ '@smithy/middleware-content-length': 4.0.4
+ '@smithy/middleware-endpoint': 4.1.13
+ '@smithy/middleware-retry': 4.1.14
+ '@smithy/middleware-serde': 4.0.8
+ '@smithy/middleware-stack': 4.0.4
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/node-http-handler': 4.0.6
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ '@smithy/url-parser': 4.0.4
+ '@smithy/util-base64': 4.0.0
+ '@smithy/util-body-length-browser': 4.0.0
+ '@smithy/util-body-length-node': 4.0.0
+ '@smithy/util-defaults-mode-browser': 4.0.21
+ '@smithy/util-defaults-mode-node': 4.0.21
+ '@smithy/util-endpoints': 3.0.6
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-retry': 4.0.6
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/region-config-resolver@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/types': 4.3.1
+ '@smithy/util-config-provider': 4.0.0
+ '@smithy/util-middleware': 4.0.4
+ tslib: 2.8.1
+
+ '@aws-sdk/signature-v4-multi-region@3.840.0':
+ dependencies:
+ '@aws-sdk/middleware-sdk-s3': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/signature-v4': 5.1.2
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/token-providers@3.840.0':
+ dependencies:
+ '@aws-sdk/core': 3.840.0
+ '@aws-sdk/nested-clients': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/property-provider': 4.0.4
+ '@smithy/shared-ini-file-loader': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/types@3.840.0':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/util-arn-parser@3.804.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@aws-sdk/util-endpoints@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/types': 4.3.1
+ '@smithy/util-endpoints': 3.0.6
+ tslib: 2.8.1
+
+ '@aws-sdk/util-locate-window@3.804.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@aws-sdk/util-user-agent-browser@3.840.0':
+ dependencies:
+ '@aws-sdk/types': 3.840.0
+ '@smithy/types': 4.3.1
+ bowser: 2.11.0
+ tslib: 2.8.1
+
+ '@aws-sdk/util-user-agent-node@3.840.0':
+ dependencies:
+ '@aws-sdk/middleware-user-agent': 3.840.0
+ '@aws-sdk/types': 3.840.0
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@aws-sdk/xml-builder@3.821.0':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
'@babel/code-frame@7.10.4':
dependencies:
'@babel/highlight': 7.25.9
@@ -19332,6 +20186,339 @@ snapshots:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
+ '@smithy/abort-controller@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/chunked-blob-reader-native@4.0.0':
+ dependencies:
+ '@smithy/util-base64': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/chunked-blob-reader@5.0.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/config-resolver@4.1.4':
+ dependencies:
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/types': 4.3.1
+ '@smithy/util-config-provider': 4.0.0
+ '@smithy/util-middleware': 4.0.4
+ tslib: 2.8.1
+
+ '@smithy/core@3.6.0':
+ dependencies:
+ '@smithy/middleware-serde': 4.0.8
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ '@smithy/util-base64': 4.0.0
+ '@smithy/util-body-length-browser': 4.0.0
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-stream': 4.2.2
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/credential-provider-imds@4.0.6':
+ dependencies:
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/property-provider': 4.0.4
+ '@smithy/types': 4.3.1
+ '@smithy/url-parser': 4.0.4
+ tslib: 2.8.1
+
+ '@smithy/eventstream-codec@4.0.4':
+ dependencies:
+ '@aws-crypto/crc32': 5.2.0
+ '@smithy/types': 4.3.1
+ '@smithy/util-hex-encoding': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-browser@4.0.4':
+ dependencies:
+ '@smithy/eventstream-serde-universal': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-config-resolver@4.1.2':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-node@4.0.4':
+ dependencies:
+ '@smithy/eventstream-serde-universal': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-universal@4.0.4':
+ dependencies:
+ '@smithy/eventstream-codec': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/fetch-http-handler@5.0.4':
+ dependencies:
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/querystring-builder': 4.0.4
+ '@smithy/types': 4.3.1
+ '@smithy/util-base64': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/hash-blob-browser@4.0.4':
+ dependencies:
+ '@smithy/chunked-blob-reader': 5.0.0
+ '@smithy/chunked-blob-reader-native': 4.0.0
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/hash-node@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ '@smithy/util-buffer-from': 4.0.0
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/hash-stream-node@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/invalid-dependency@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/is-array-buffer@2.2.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/is-array-buffer@4.0.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/md5-js@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/middleware-content-length@4.0.4':
+ dependencies:
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/middleware-endpoint@4.1.13':
+ dependencies:
+ '@smithy/core': 3.6.0
+ '@smithy/middleware-serde': 4.0.8
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/shared-ini-file-loader': 4.0.4
+ '@smithy/types': 4.3.1
+ '@smithy/url-parser': 4.0.4
+ '@smithy/util-middleware': 4.0.4
+ tslib: 2.8.1
+
+ '@smithy/middleware-retry@4.1.14':
+ dependencies:
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/service-error-classification': 4.0.6
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-retry': 4.0.6
+ tslib: 2.8.1
+ uuid: 9.0.1
+
+ '@smithy/middleware-serde@4.0.8':
+ dependencies:
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/middleware-stack@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/node-config-provider@4.1.3':
+ dependencies:
+ '@smithy/property-provider': 4.0.4
+ '@smithy/shared-ini-file-loader': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/node-http-handler@4.0.6':
+ dependencies:
+ '@smithy/abort-controller': 4.0.4
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/querystring-builder': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/property-provider@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/protocol-http@5.1.2':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/querystring-builder@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ '@smithy/util-uri-escape': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/querystring-parser@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/service-error-classification@4.0.6':
+ dependencies:
+ '@smithy/types': 4.3.1
+
+ '@smithy/shared-ini-file-loader@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/signature-v4@5.1.2':
+ dependencies:
+ '@smithy/is-array-buffer': 4.0.0
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ '@smithy/util-hex-encoding': 4.0.0
+ '@smithy/util-middleware': 4.0.4
+ '@smithy/util-uri-escape': 4.0.0
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/smithy-client@4.4.5':
+ dependencies:
+ '@smithy/core': 3.6.0
+ '@smithy/middleware-endpoint': 4.1.13
+ '@smithy/middleware-stack': 4.0.4
+ '@smithy/protocol-http': 5.1.2
+ '@smithy/types': 4.3.1
+ '@smithy/util-stream': 4.2.2
+ tslib: 2.8.1
+
+ '@smithy/types@4.3.1':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/url-parser@4.0.4':
+ dependencies:
+ '@smithy/querystring-parser': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/util-base64@4.0.0':
+ dependencies:
+ '@smithy/util-buffer-from': 4.0.0
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/util-body-length-browser@4.0.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/util-body-length-node@4.0.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/util-buffer-from@2.2.0':
+ dependencies:
+ '@smithy/is-array-buffer': 2.2.0
+ tslib: 2.8.1
+
+ '@smithy/util-buffer-from@4.0.0':
+ dependencies:
+ '@smithy/is-array-buffer': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/util-config-provider@4.0.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/util-defaults-mode-browser@4.0.21':
+ dependencies:
+ '@smithy/property-provider': 4.0.4
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ bowser: 2.11.0
+ tslib: 2.8.1
+
+ '@smithy/util-defaults-mode-node@4.0.21':
+ dependencies:
+ '@smithy/config-resolver': 4.1.4
+ '@smithy/credential-provider-imds': 4.0.6
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/property-provider': 4.0.4
+ '@smithy/smithy-client': 4.4.5
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/util-endpoints@3.0.6':
+ dependencies:
+ '@smithy/node-config-provider': 4.1.3
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/util-hex-encoding@4.0.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/util-middleware@4.0.4':
+ dependencies:
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/util-retry@4.0.6':
+ dependencies:
+ '@smithy/service-error-classification': 4.0.6
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
+ '@smithy/util-stream@4.2.2':
+ dependencies:
+ '@smithy/fetch-http-handler': 5.0.4
+ '@smithy/node-http-handler': 4.0.6
+ '@smithy/types': 4.3.1
+ '@smithy/util-base64': 4.0.0
+ '@smithy/util-buffer-from': 4.0.0
+ '@smithy/util-hex-encoding': 4.0.0
+ '@smithy/util-utf8': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/util-uri-escape@4.0.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/util-utf8@2.3.0':
+ dependencies:
+ '@smithy/util-buffer-from': 2.2.0
+ tslib: 2.8.1
+
+ '@smithy/util-utf8@4.0.0':
+ dependencies:
+ '@smithy/util-buffer-from': 4.0.0
+ tslib: 2.8.1
+
+ '@smithy/util-waiter@4.0.6':
+ dependencies:
+ '@smithy/abort-controller': 4.0.4
+ '@smithy/types': 4.3.1
+ tslib: 2.8.1
+
'@surma/rollup-plugin-off-main-thread@2.2.3':
dependencies:
ejs: 3.1.10
@@ -19885,6 +21072,8 @@ snapshots:
'@types/unist@3.0.3': {}
+ '@types/uuid@9.0.8': {}
+
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.15.30
@@ -20647,6 +21836,8 @@ snapshots:
boolbase@1.0.0: {}
+ bowser@2.11.0: {}
+
boxen@6.2.1:
dependencies:
ansi-align: 3.0.1
@@ -22740,6 +23931,10 @@ snapshots:
fast-uri@3.0.6: {}
+ fast-xml-parser@4.4.1:
+ dependencies:
+ strnum: 1.1.2
+
fastest-levenshtein@1.0.16: {}
fastq@1.19.1:
@@ -29252,6 +30447,8 @@ snapshots:
dependencies:
js-tokens: 9.0.1
+ strnum@1.1.2: {}
+
structured-headers@0.4.1: {}
style-to-js@1.1.16:
@@ -29959,6 +31156,8 @@ snapshots:
uuid@8.3.2: {}
+ uuid@9.0.1: {}
+
uvu@0.5.6:
dependencies:
dequal: 2.0.3