diff options
| -rw-r--r-- | docs/docs/03-configuration.md | 41 | ||||
| -rw-r--r-- | packages/api/utils/assets.ts | 4 | ||||
| -rw-r--r-- | packages/api/utils/upload.ts | 2 | ||||
| -rw-r--r-- | packages/e2e_tests/docker-compose.yml | 15 | ||||
| -rw-r--r-- | packages/e2e_tests/package.json | 5 | ||||
| -rw-r--r-- | packages/e2e_tests/setup/startContainers.ts | 4 | ||||
| -rw-r--r-- | packages/e2e_tests/tests/assetdb/assetdb-utils.ts | 289 | ||||
| -rw-r--r-- | packages/e2e_tests/tests/assetdb/interface-compliance.test.ts | 627 | ||||
| -rw-r--r-- | packages/e2e_tests/tests/assetdb/local-filesystem-store.test.ts | 228 | ||||
| -rw-r--r-- | packages/e2e_tests/tests/assetdb/s3-store.test.ts | 197 | ||||
| -rw-r--r-- | packages/shared/assetdb.ts | 670 | ||||
| -rw-r--r-- | packages/shared/config.ts | 21 | ||||
| -rw-r--r-- | packages/shared/package.json | 1 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 1199 |
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 |
