From 0b8784a22463f8430b19de6b4412fbbb0741990c Mon Sep 17 00:00:00 2001 From: Zeemnew <287525836+Zeemnew@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:02:04 +0100 Subject: [PATCH] feat(infra): provision MinIO object storage for local S3-compatible dev Add MinIO to docker-compose with private bucket bootstrap, wire backend OBJECT_STORE_* env config, and implement a shared S3 client path for MinIO, AWS S3, and Cloudflare R2. Closes #223 --- .env.example | 10 + apps/backend/package.json | 1 + apps/backend/src/__tests__/config.test.ts | 23 + .../backend/src/__tests__/objectStore.test.ts | 74 +++ apps/backend/src/__tests__/setup.ts | 6 + apps/backend/src/config.ts | 10 + apps/backend/src/index.ts | 16 +- apps/backend/src/lib/objectStore.ts | 80 +++ infra/docker-compose.yml | 37 ++ pnpm-lock.yaml | 480 +++++++++++++++++- 10 files changed, 708 insertions(+), 29 deletions(-) create mode 100644 apps/backend/src/__tests__/objectStore.test.ts create mode 100644 apps/backend/src/lib/objectStore.ts diff --git a/.env.example b/.env.example index 6cf1885..b470eab 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,16 @@ DATABASE_URL= # Redis REDIS_URL= +# Object storage (S3-compatible). Defaults below target local MinIO from +# infra/docker-compose.yml. For AWS S3 or Cloudflare R2, swap endpoint, +# credentials, region, and set OBJECT_STORE_FORCE_PATH_STYLE=false. +OBJECT_STORE_ENDPOINT=http://localhost:9000 +OBJECT_STORE_BUCKET=clicked +OBJECT_STORE_ACCESS_KEY=clicked +OBJECT_STORE_SECRET_KEY=clickedsecret +OBJECT_STORE_REGION=us-east-1 +OBJECT_STORE_FORCE_PATH_STYLE=true + # AI Service OPENAI_API_KEY= diff --git a/apps/backend/package.json b/apps/backend/package.json index 5d8b3bd..9d3be1f 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -25,6 +25,7 @@ "license": "ISC", "packageManager": "pnpm@10.28.1", "dependencies": { + "@aws-sdk/client-s3": "^3.1075.0", "@socket.io/redis-adapter": "^8.3.0", "@stellar/stellar-sdk": "^15.1.0", "cors": "^2.8.6", diff --git a/apps/backend/src/__tests__/config.test.ts b/apps/backend/src/__tests__/config.test.ts index cf4a381..5dbe8a1 100644 --- a/apps/backend/src/__tests__/config.test.ts +++ b/apps/backend/src/__tests__/config.test.ts @@ -7,6 +7,12 @@ const validEnv = { JWT_SECRET: 'test-secret', PORT: '3001', TOKEN_TRANSFER_CONTRACT_ID: 'CONTRACT123', + OBJECT_STORE_ENDPOINT: 'http://localhost:9000', + OBJECT_STORE_BUCKET: 'clicked', + OBJECT_STORE_ACCESS_KEY: 'clicked', + OBJECT_STORE_SECRET_KEY: 'clickedsecret', + OBJECT_STORE_REGION: 'us-east-1', + OBJECT_STORE_FORCE_PATH_STYLE: 'true', }; describe('loadEnv', () => { @@ -26,6 +32,12 @@ describe('loadEnv', () => { JWT_SECRET: 'test-secret', PORT: 3001, TOKEN_TRANSFER_CONTRACT_ID: 'CONTRACT123', + OBJECT_STORE_ENDPOINT: 'http://localhost:9000', + OBJECT_STORE_BUCKET: 'clicked', + OBJECT_STORE_ACCESS_KEY: 'clicked', + OBJECT_STORE_SECRET_KEY: 'clickedsecret', + OBJECT_STORE_REGION: 'us-east-1', + OBJECT_STORE_FORCE_PATH_STYLE: true, }); expect(errorSpy).not.toHaveBeenCalled(); expect(logSpy).not.toHaveBeenCalled(); @@ -78,4 +90,15 @@ describe('loadEnv', () => { const parsed = EnvSchema.parse({ ...validEnv, PORT: '8080' }); expect(parsed.PORT).toBe(8080); }); + + it('coerces OBJECT_STORE_FORCE_PATH_STYLE from string to boolean', () => { + expect( + EnvSchema.parse({ ...validEnv, OBJECT_STORE_FORCE_PATH_STYLE: 'false' }) + .OBJECT_STORE_FORCE_PATH_STYLE, + ).toBe(false); + expect( + EnvSchema.parse({ ...validEnv, OBJECT_STORE_FORCE_PATH_STYLE: 'true' }) + .OBJECT_STORE_FORCE_PATH_STYLE, + ).toBe(true); + }); }); diff --git a/apps/backend/src/__tests__/objectStore.test.ts b/apps/backend/src/__tests__/objectStore.test.ts new file mode 100644 index 0000000..c1ccd1c --- /dev/null +++ b/apps/backend/src/__tests__/objectStore.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + DeleteObjectCommand, + GetObjectCommand, + HeadBucketCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import { createObjectStore, createObjectStoreClient } from '../lib/objectStore.js'; + +const config = { + OBJECT_STORE_ENDPOINT: 'http://localhost:9000', + OBJECT_STORE_BUCKET: 'clicked', + OBJECT_STORE_ACCESS_KEY: 'clicked', + OBJECT_STORE_SECRET_KEY: 'clickedsecret', + OBJECT_STORE_REGION: 'us-east-1', + OBJECT_STORE_FORCE_PATH_STYLE: true, +}; + +describe('createObjectStoreClient', () => { + it('configures the S3 client for path-style MinIO endpoints', () => { + const client = createObjectStoreClient(config); + expect(client).toBeInstanceOf(S3Client); + expect(client.config.endpoint).toBeDefined(); + }); + + it('supports virtual-hosted AWS/R2 style endpoints when path style is disabled', () => { + const client = createObjectStoreClient({ + ...config, + OBJECT_STORE_ENDPOINT: 'https://s3.amazonaws.com', + OBJECT_STORE_FORCE_PATH_STYLE: false, + }); + expect(client).toBeInstanceOf(S3Client); + }); +}); + +describe('ObjectStore', () => { + const send = vi.fn(); + + beforeEach(() => { + send.mockReset(); + vi.spyOn(S3Client.prototype, 'send').mockImplementation(send); + }); + + it('checks bucket reachability with HeadBucket', async () => { + send.mockResolvedValue({}); + const store = createObjectStore(config); + + await store.ensureBucketReachable(); + + expect(send).toHaveBeenCalledWith(expect.any(HeadBucketCommand)); + }); + + it('uploads, reads, and deletes objects in the configured bucket', async () => { + send.mockResolvedValue({}); + const store = createObjectStore(config); + + await store.putObject('avatars/user.png', Buffer.from('png'), 'image/png'); + await store.getObject('avatars/user.png'); + await store.deleteObject('avatars/user.png'); + + expect(send).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + input: expect.objectContaining({ + Bucket: 'clicked', + Key: 'avatars/user.png', + ContentType: 'image/png', + }), + }), + ); + expect(send).toHaveBeenNthCalledWith(2, expect.any(GetObjectCommand)); + expect(send).toHaveBeenNthCalledWith(3, expect.any(DeleteObjectCommand)); + }); +}); diff --git a/apps/backend/src/__tests__/setup.ts b/apps/backend/src/__tests__/setup.ts index d48b670..da15208 100644 --- a/apps/backend/src/__tests__/setup.ts +++ b/apps/backend/src/__tests__/setup.ts @@ -1,2 +1,8 @@ process.env['JWT_SECRET'] = 'test-secret-for-ci-only'; process.env['DATABASE_URL'] = 'postgres://localhost/test'; +process.env['OBJECT_STORE_ENDPOINT'] = 'http://localhost:9000'; +process.env['OBJECT_STORE_BUCKET'] = 'clicked'; +process.env['OBJECT_STORE_ACCESS_KEY'] = 'clicked'; +process.env['OBJECT_STORE_SECRET_KEY'] = 'clickedsecret'; +process.env['OBJECT_STORE_REGION'] = 'us-east-1'; +process.env['OBJECT_STORE_FORCE_PATH_STYLE'] = 'true'; diff --git a/apps/backend/src/config.ts b/apps/backend/src/config.ts index ce53b6c..cd713b4 100644 --- a/apps/backend/src/config.ts +++ b/apps/backend/src/config.ts @@ -1,5 +1,9 @@ import { z } from 'zod'; +const booleanEnv = z + .enum(['true', 'false', '1', '0']) + .transform((value) => value === 'true' || value === '1'); + /** * Startup environment schema. Every variable here is required for the * backend to boot; `loadEnv` validates `process.env` against it and exits @@ -11,6 +15,12 @@ export const EnvSchema = z.object({ JWT_SECRET: z.string().min(1, 'JWT_SECRET is required'), PORT: z.coerce.number().int('PORT must be an integer').positive('PORT must be positive'), TOKEN_TRANSFER_CONTRACT_ID: z.string().min(1, 'TOKEN_TRANSFER_CONTRACT_ID is required'), + OBJECT_STORE_ENDPOINT: z.string().min(1, 'OBJECT_STORE_ENDPOINT is required'), + OBJECT_STORE_BUCKET: z.string().min(1, 'OBJECT_STORE_BUCKET is required'), + OBJECT_STORE_ACCESS_KEY: z.string().min(1, 'OBJECT_STORE_ACCESS_KEY is required'), + OBJECT_STORE_SECRET_KEY: z.string().min(1, 'OBJECT_STORE_SECRET_KEY is required'), + OBJECT_STORE_REGION: z.string().min(1, 'OBJECT_STORE_REGION is required'), + OBJECT_STORE_FORCE_PATH_STYLE: booleanEnv, }); export type Env = z.infer; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index f8d60b7..b045e3e 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -18,12 +18,14 @@ import { runForever as runStellarListener, } from './services/stellarListener.js'; import { loadEnv } from './config.js'; +import { createObjectStore } from './lib/objectStore.js'; dotenv.config(); // Validate required environment variables at boot. Exits with code 1 and // logs the offending vars if anything is missing or malformed. -loadEnv(); +const env = loadEnv(); +export const objectStore = createObjectStore(env); const httpServer = createServer(app); const io = new Server(httpServer, { @@ -123,6 +125,18 @@ httpServer.listen(PORT, () => { // Redis is unreachable; on failure we fall back to the in-process adapter. void attachRedisAdapter(); +// #223 — Verify object storage is reachable when the API boots. Logs a +// warning instead of crashing so unit tests and partial local setups still run. +void objectStore + .ensureBucketReachable() + .then(() => { + console.log(`[object-store] bucket "${env.OBJECT_STORE_BUCKET}" reachable`); + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + console.warn(`[object-store] bucket unreachable (${message})`); + }); + // #46 — Stellar transfer event listener. Only spin up when the contract // id is configured so local-dev and unit-test runs don't try to talk to // Soroban RPC. The listener never throws out of runForever, so a failed diff --git a/apps/backend/src/lib/objectStore.ts b/apps/backend/src/lib/objectStore.ts new file mode 100644 index 0000000..726f07e --- /dev/null +++ b/apps/backend/src/lib/objectStore.ts @@ -0,0 +1,80 @@ +import { + DeleteObjectCommand, + GetObjectCommand, + HeadBucketCommand, + PutObjectCommand, + S3Client, + type PutObjectCommandInput, +} from '@aws-sdk/client-s3'; +import type { Env } from '../config.js'; + +export type ObjectStoreConfig = Pick< + Env, + | 'OBJECT_STORE_ENDPOINT' + | 'OBJECT_STORE_BUCKET' + | 'OBJECT_STORE_ACCESS_KEY' + | 'OBJECT_STORE_SECRET_KEY' + | 'OBJECT_STORE_REGION' + | 'OBJECT_STORE_FORCE_PATH_STYLE' +>; + +/** + * Build an S3-compatible client from env. The same configuration works against + * local MinIO (path-style + custom endpoint), AWS S3, and Cloudflare R2 — + * only the env values change. + */ +export function createObjectStoreClient(config: ObjectStoreConfig): S3Client { + return new S3Client({ + endpoint: config.OBJECT_STORE_ENDPOINT, + region: config.OBJECT_STORE_REGION, + credentials: { + accessKeyId: config.OBJECT_STORE_ACCESS_KEY, + secretAccessKey: config.OBJECT_STORE_SECRET_KEY, + }, + forcePathStyle: config.OBJECT_STORE_FORCE_PATH_STYLE, + }); +} + +export class ObjectStore { + constructor( + private readonly client: S3Client, + private readonly bucket: string, + ) {} + + async ensureBucketReachable(): Promise { + await this.client.send(new HeadBucketCommand({ Bucket: this.bucket })); + } + + async putObject(key: string, body: NonNullable, contentType?: string) { + await this.client.send( + new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + Body: body, + ...(contentType ? { ContentType: contentType } : {}), + }), + ); + } + + async getObject(key: string) { + return this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }), + ); + } + + async deleteObject(key: string) { + await this.client.send( + new DeleteObjectCommand({ + Bucket: this.bucket, + Key: key, + }), + ); + } +} + +export function createObjectStore(config: ObjectStoreConfig): ObjectStore { + return new ObjectStore(createObjectStoreClient(config), config.OBJECT_STORE_BUCKET); +} diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 5ef96d8..924e085 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -37,6 +37,43 @@ services: retries: 5 start_period: 2s + # #223 — S3-compatible object storage (MinIO). Swap env vars to target AWS S3 + # or Cloudflare R2 in production; the backend uses the same S3 client path. + minio: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: clicked + MINIO_ROOT_PASSWORD: clickedsecret + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + # Create the application bucket on first boot with a private ACL (no anonymous + # read). Re-runs are idempotent via --ignore-existing. + minio-init: + image: minio/mc:RELEASE.2025-04-16T18-13-26Z + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set local http://minio:9000 clicked clickedsecret && + mc mb --ignore-existing local/clicked && + mc anonymous set none local/clicked + " + restart: "no" + volumes: postgres_data: redis_data: + minio_data: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54918ed..7e6c908 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,13 @@ importers: version: 3.8.3 turbo: specifier: latest - version: 2.9.16 + version: 2.10.0 apps/backend: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1075.0 + version: 3.1075.0 '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.6) @@ -170,6 +173,109 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@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/checksums@3.1000.8': + resolution: {integrity: sha512-v0U9S7gBIme3OTgt1LdbAF4RpvavCc+4GK1+1xqAcqtbrHsEhjQo6R45LKcjhs/+WrRJij1Y0Gztw7QPAIeUfA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1075.0': + resolution: {integrity: sha512-h1A6nIl1YX6Y45enGsTK7ef3ZrOnBiQJ1qF5R2K/nMWfsu6A9mc2Y5T66nxerABzyjjyyvign3MrzafnFoQKmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.23': + resolution: {integrity: sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.49': + resolution: {integrity: sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.51': + resolution: {integrity: sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.56': + resolution: {integrity: sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.55': + resolution: {integrity: sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.58': + resolution: {integrity: sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.49': + resolution: {integrity: sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.55': + resolution: {integrity: sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.55': + resolution: {integrity: sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.33': + resolution: {integrity: sha512-qMgQSPemQq2/eW/e/0+SpY4kYR5L7dUgBiVdEc5bd+ztHNv07ZMYiI+sTiir3TgKndFfglSw/VFi7oZJ6bZ63g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.54': + resolution: {integrity: sha512-GDfDQ0gwLFRKN9gWIKcmVrHJ3e7XagnY7N1LLzMVNgnOnuY7f/ALgmy3CuBjosWD95T/Z6e+gs1IeWmLPkyLKQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.23': + resolution: {integrity: sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1074.0': + resolution: {integrity: sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.31': + resolution: {integrity: sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1164,6 +1270,42 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@smithy/core@3.26.0': + resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.2': + resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.5.2': + resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==} + 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/node-http-handler@4.8.2': + resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.5.2': + resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + 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-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -1302,33 +1444,33 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@turbo/darwin-64@2.9.16': - resolution: {integrity: sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw==} + '@turbo/darwin-64@2.10.0': + resolution: {integrity: sha512-EwvHThXzpY0KGd1/NAmuewI5D+aVa3Rl/OlxE36yfjUKb/+ySrfJrSlEFt8aD1OXwnnaHnQnPKHFndor0Zxlsg==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.16': - resolution: {integrity: sha512-YPgrn+5HIGzrx0O2a631SV4MBQUe4W/DafMFUuBVgaU32PW9/OTT0ehviF0QSxTXuRJlHvW2eUTemddF5/spmw==} + '@turbo/darwin-arm64@2.10.0': + resolution: {integrity: sha512-9d2fTyyG0lf5Wq1bwJA9qUaeecViMkLcdctWaMMmCkxZ/JqypmqOwK3W6vmejeKVgkr06gSoiX8bD+xN5Jpxcg==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.16': - resolution: {integrity: sha512-vAEf1H6l26lTpl9FJ/peQo1NUB8RC0sbEJJz5mPcUhHA2bPDup2x3CZPgo/bH8S4cUcBLm4FN3UHd5iUO2RAew==} + '@turbo/linux-64@2.10.0': + resolution: {integrity: sha512-sZBtjMuufitanjzi6UssoUpJMnnPlLMcdcJj3m3ptNsSq31Xh7MnjhwA5nWvLDTfEFg8GPcbYFXMo8vSdKRfqQ==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.16': - resolution: {integrity: sha512-xDBLR2PZg4BrQOchfG6svgpv5FCNJ2TOtT2psLdEJcdKo1BH+pnPs9Xj6pvUjgfkHbuvBOfeE4R6tvxMoQKDHQ==} + '@turbo/linux-arm64@2.10.0': + resolution: {integrity: sha512-vkq/Z8R+1DQ+kifWFa810IjRy2NNBVvha3cg9sWA3nFh6nnGrHSMnnJKrzH7c/No9kq4Jb55Ru44YKsCSBgrKg==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.16': - resolution: {integrity: sha512-NBAJnaUiGdgkSzQwUIdOvkCkcpTSu58G/sBGa0mvBtzfvFOOgrQwepKOOQ8cp6sWM6OcKDNFj2p1dsZA1OWjPg==} + '@turbo/windows-64@2.10.0': + resolution: {integrity: sha512-CRUEguLWxFQHptYZS7HjPhNhAFawfea07iR+xAQ5e4klgLrPCMdexBkXwSCwOxqTFknJ7RZFN3gOaADsw+Gttg==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.16': - resolution: {integrity: sha512-Y7SJppD0Z8wjO3Ec0ZGd9KQ4Yv0BMnA8CIowj5Vp+OEVsosXDG2weK6/t1RRLfJmc2Ozrnd6y4DOgQys+mn3WQ==} + '@turbo/windows-arm64@2.10.0': + resolution: {integrity: sha512-dVHGaf9F8twzgibcBqKoADT/LLqf9++jDb+hq/LPWWaOmRpp4M+/pVOm7vy4z9D++xg8eaxWLT0+wQxFwhYu9A==} cpu: [arm64] os: [win32] @@ -1825,6 +1967,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -3583,8 +3728,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo@2.9.16: - resolution: {integrity: sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg==} + turbo@2.10.0: + resolution: {integrity: sha512-o016H9PPtuH2deb3mh3Vci3Avfi9UYgM/RONQisY7HnloupP0IFSbFS3gFYJgFJP8nwBrByHWFQIDa8T2zIXPw==} hasBin: true tweetnacl@1.0.3: @@ -3842,6 +3987,235 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + 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.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@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.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@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.973.13 + 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.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/checksums@3.1000.8': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1075.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.974.23 + '@aws-sdk/credential-provider-node': 3.972.58 + '@aws-sdk/middleware-flexible-checksums': 3.974.33 + '@aws-sdk/middleware-sdk-s3': 3.972.54 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.23': + dependencies: + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.31 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.26.0 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.51': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.56': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-login': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.58': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-ini': 3.972.56 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/token-providers': 3.1074.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.33': + dependencies: + '@aws-sdk/checksums': 3.1000.8 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.23': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.35': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1074.0': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.31': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4541,6 +4915,54 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@smithy/core@3.26.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.8.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/types@4.15.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-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.6)': @@ -4680,22 +5102,22 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@turbo/darwin-64@2.9.16': + '@turbo/darwin-64@2.10.0': optional: true - '@turbo/darwin-arm64@2.9.16': + '@turbo/darwin-arm64@2.10.0': optional: true - '@turbo/linux-64@2.9.16': + '@turbo/linux-64@2.10.0': optional: true - '@turbo/linux-arm64@2.9.16': + '@turbo/linux-arm64@2.10.0': optional: true - '@turbo/windows-64@2.9.16': + '@turbo/windows-64@2.10.0': optional: true - '@turbo/windows-arm64@2.9.16': + '@turbo/windows-arm64@2.10.0': optional: true '@tybys/wasm-util@0.10.1': @@ -5278,6 +5700,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.14.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -7302,14 +7726,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo@2.9.16: + turbo@2.10.0: optionalDependencies: - '@turbo/darwin-64': 2.9.16 - '@turbo/darwin-arm64': 2.9.16 - '@turbo/linux-64': 2.9.16 - '@turbo/linux-arm64': 2.9.16 - '@turbo/windows-64': 2.9.16 - '@turbo/windows-arm64': 2.9.16 + '@turbo/darwin-64': 2.10.0 + '@turbo/darwin-arm64': 2.10.0 + '@turbo/linux-64': 2.10.0 + '@turbo/linux-arm64': 2.10.0 + '@turbo/windows-64': 2.10.0 + '@turbo/windows-arm64': 2.10.0 tweetnacl@1.0.3: {}