diff --git a/packages/storage/jest.config.cjs b/packages/storage/jest.config.cjs new file mode 100644 index 0000000..02a4382 --- /dev/null +++ b/packages/storage/jest.config.cjs @@ -0,0 +1,35 @@ +/** @type {import('jest').Config} */ +// packages/storage/jest.config.cjs —— 真实 MinIO 集成测试(*.spec.ts,compose 起 +// photo-live-minio-1)。ts-jest 保持类型检查模式(非 isolatedModules)。 +// env 约定:setupFiles 经 test/load-env.ts 向上定位仓库根 .env(唯一 env 文件)。 +// @photo/contracts 为 ESM 包且是本包运行时依赖:moduleNameMapper 指向其 TS 源码, +// 由 ts-jest 以 CJS 编译(.js -> $1 映射同 packages/contracts/jest.config.cjs), +// 与 services/api/jest.config.ts 的消费方式一致。 +module.exports = { + testEnvironment: 'node', + rootDir: '.', + setupFiles: ['./test/load-env.ts'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: { + module: 'commonjs', + moduleResolution: 'node10', + target: 'es2022', + lib: ['es2022'], + strict: true, + esModuleInterop: true, + skipLibCheck: true, + types: ['node', 'jest'], + }, + }, + ], + }, + moduleNameMapper: { + '^@photo/contracts$': '/../../packages/contracts/src/index.ts', + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + testMatch: ['/test/**/*.spec.ts'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], +}; diff --git a/packages/storage/package.json b/packages/storage/package.json new file mode 100644 index 0000000..aedb64d --- /dev/null +++ b/packages/storage/package.json @@ -0,0 +1,34 @@ +{ + "name": "@photo/storage", + "version": "0.1.0", + "private": true, + "description": "StoragePort 抽象 + MinIO 适配器(presigned PUT/GET、head、流式读取、禁止覆盖语义的 copyIfAbsent;P5/G1 生产 OSS 适配器实现同一端口)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc --noEmit", + "test": "jest", + "lint": "eslint ." + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", + "@aws-sdk/s3-request-presigner": "^3.700.0", + "@photo/contracts": "workspace:*" + }, + "devDependencies": { + "@types/jest": "^29.5.13", + "@types/node": "^22.10.0", + "dotenv": "^16.4.5", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "typescript": "^5.6.0" + } +} diff --git a/packages/storage/src/errors.ts b/packages/storage/src/errors.ts new file mode 100644 index 0000000..6d11ce9 --- /dev/null +++ b/packages/storage/src/errors.ts @@ -0,0 +1,16 @@ +// packages/storage/src/errors.ts —— 存储层错误:携带契约错误码(如 +// UPLOAD_VERIFY_FAILED),api 全局异常过滤器据此映射为错误 envelope。 +import { ErrorCodes, type ErrorCode } from '@photo/contracts'; + +export class StorageError extends Error { + /** 契约稳定错误码(packages/contracts/src/errors.ts),非存储实现细节。 */ + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = 'StorageError'; + this.code = code; + } +} + +export { ErrorCodes }; diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts new file mode 100644 index 0000000..f4bd71d --- /dev/null +++ b/packages/storage/src/index.ts @@ -0,0 +1,32 @@ +// packages/storage/src/index.ts —— 存储包统一出口:端口类型 + MinIO 适配器 + 错误 + +// env 工厂。api(Task 18 presign/verify)与 worker(Task 20 variants put/get)经 +// `createStorageFromEnv()` 构造实例,测试/本地指向 MinIO,生产指向 OSS。 +// commonjs 包(消费方 api/worker 均为 CJS);@photo/contracts 为 ESM dist,运行时经 +// Node require(esm) 消费(Node >= 22.12,见根 engines)。 +import { MinioStorageAdapter, type MinioStorageConfig } from './minio.adapter'; + +export { MinioStorageAdapter } from './minio.adapter'; +export type { MinioStorageConfig } from './minio.adapter'; +export { StorageError, ErrorCodes } from './errors'; +export type { StoragePort, UploadCredential } from './port'; + +/** + * 从 S3_* 环境变量构造 MinIO/S3 兼容适配器(仓库根 .env 经 dotenv-cli 或 + * test/load-env.ts 注入)。任一变量缺失即抛错,禁止静默回退默认值。 + */ +export function createStorageFromEnv(): MinioStorageAdapter { + const config = { + endpoint: process.env.S3_ENDPOINT, + region: process.env.S3_REGION, + accessKey: process.env.S3_ACCESS_KEY, + secretKey: process.env.S3_SECRET_KEY, + bucket: process.env.S3_BUCKET, + } satisfies Record; + const missing = Object.entries(config) + .filter(([, value]) => !value) + .map(([key]) => `S3_${key.toUpperCase()}`); + if (missing.length > 0) { + throw new Error(`createStorageFromEnv: 缺少环境变量 ${missing.join(', ')}(S3_* 配置见仓库根 .env)`); + } + return new MinioStorageAdapter(config as MinioStorageConfig); +} diff --git a/packages/storage/src/minio.adapter.ts b/packages/storage/src/minio.adapter.ts new file mode 100644 index 0000000..ba344b3 --- /dev/null +++ b/packages/storage/src/minio.adapter.ts @@ -0,0 +1,142 @@ +// packages/storage/src/minio.adapter.ts —— StoragePort 的 MinIO 实现(S3 兼容协议, +// AWS SDK v3 + path-style)。生产 OSS 适配器(P5/G1)实现同端口,行为契约以此处注释为准。 +import { + CopyObjectCommand, + GetObjectCommand, + HeadObjectCommand, + PutObjectCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { Readable } from 'node:stream'; +import { ErrorCodes, StorageError } from './errors'; +import type { StoragePort, UploadCredential } from './port'; + +export interface MinioStorageConfig { + endpoint: string; + region: string; + accessKey: string; + secretKey: string; + bucket: string; +} + +/** S3 错误统一判定:HeadObject 对缺失键返回 404/NotFound(无 body 错误码),GetObject 为 NoSuchKey。 */ +function isNotFound(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false; + const e = error as { name?: string; $metadata?: { httpStatusCode?: number } }; + return e.$metadata?.httpStatusCode === 404 || e.name === 'NotFound' || e.name === 'NoSuchKey'; +} + +export class MinioStorageAdapter implements StoragePort { + private readonly client: S3Client; + private readonly bucket: string; + + constructor(config: MinioStorageConfig) { + const missing = (Object.keys(config) as Array).filter((k) => !config[k]); + if (missing.length > 0) { + throw new Error(`MinioStorageAdapter: 存储配置缺失 ${missing.join(', ')}(S3_* 环境变量见仓库根 .env)`); + } + this.bucket = config.bucket; + this.client = new S3Client({ + endpoint: config.endpoint, + region: config.region, + credentials: { accessKeyId: config.accessKey, secretAccessKey: config.secretKey }, + forcePathStyle: true, + }); + } + + /** + * 预签名上传。S3 预签名无法对 Content-Length 签名(SigV4 规范排除该头), + * bytes 参数仅作为端口契约保留:字节数核验发生在上传完成后的 head/verify 阶段。 + * Content-Type 经 PutObjectCommand 的 ContentType 参数参与签名,客户端必须 + * 原样携带返回的 headers,否则 403 SignatureDoesNotMatch。 + */ + async presignPut(objectKey: string, bytes: number, contentType: string, ttlSeconds: number): Promise { + void bytes; // 见上:签名不含 Content-Length,大小核验由 verify 阶段负责 + const url = await getSignedUrl( + this.client, + new PutObjectCommand({ Bucket: this.bucket, Key: objectKey, ContentType: contentType }), + { expiresIn: ttlSeconds }, + ); + return { + mode: 'presigned', + url, + method: 'PUT', + headers: { 'Content-Type': contentType }, + expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(), + }; + } + + async presignGet(objectKey: string, ttlSeconds: number): Promise { + return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }), { + expiresIn: ttlSeconds, + }); + } + + async head(objectKey: string): Promise<{ bytes: number; etag: string } | null> { + try { + const out = await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: objectKey })); + return { bytes: out.ContentLength ?? 0, etag: out.ETag ?? '' }; + } catch (error) { + if (isNotFound(error)) return null; + throw error; + } + } + + async getStream(objectKey: string): Promise { + const out = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })); + if (!out.Body) { + throw new StorageError(ErrorCodes.UPLOAD_VERIFY_FAILED, `getStream: 对象无 Body: ${objectKey}`); + } + return out.Body as Readable; + } + + async put(objectKey: string, body: Buffer, contentType: string): Promise<{ etag: string }> { + const out = await this.client.send( + new PutObjectCommand({ Bucket: this.bucket, Key: objectKey, ContentType: contentType, Body: body }), + ); + return { etag: out.ETag ?? '' }; + } + + /** + * 禁止覆盖语义的复制(staging → 正式键): + * - dest 已存在且字节数与 expectedBytes 一致 → 幂等返回 created:false(同 etag); + * - dest 已存在但字节数不符 → StorageError(UPLOAD_VERIFY_FAILED); + * - dest 不存在 → CopyObject(MetadataDirective=COPY,MinIO 需显式指定以保留源元数据) + * 后 head 复核字节数再返回 created:true。 + * + * 已知竞态窗口:MinIO 的 CopyObject 不支持条件写(无 forbid-overwrite 头),两个并发 + * copyIfAbsent 同时通过 head 检查后,后发的 CopyObject 会覆盖先发的结果。本项目接受该 + * 窗口:同一会话的并发调用复制的是同一个 staging 源对象,覆盖结果字节一致(幂等等价); + * 跨会话撞键理论上不可能(正式键含会话/照片唯一标识)。生产 OSS 适配器将使用 + * x-oss-forbid-overwrite 头消除该窗口(G1 验收点)。 + */ + async copyIfAbsent(src: string, dest: string, expectedBytes: number): Promise<{ created: boolean; etag: string }> { + const existing = await this.head(dest); + if (existing) { + if (existing.bytes === expectedBytes) return { created: false, etag: existing.etag }; + throw new StorageError( + ErrorCodes.UPLOAD_VERIFY_FAILED, + `copyIfAbsent: dest 已存在且字节数不符(expected=${expectedBytes}, actual=${existing.bytes}): ${dest}`, + ); + } + // CopySource 须为 "bucket/key" 且 key 部分 URL 编码(保留 / 分隔符) + const copySource = [this.bucket, ...src.split('/').map(encodeURIComponent)].join('/'); + await this.client.send( + new CopyObjectCommand({ + Bucket: this.bucket, + Key: dest, + CopySource: copySource, + MetadataDirective: 'COPY', + }), + ); + const verified = await this.head(dest); + if (!verified || verified.bytes !== expectedBytes) { + throw new StorageError( + ErrorCodes.UPLOAD_VERIFY_FAILED, + `copyIfAbsent: 复制后复核失败(对象缺失或字节数不符): ${dest}`, + ); + } + return { created: true, etag: verified.etag }; + } +} diff --git a/packages/storage/src/port.ts b/packages/storage/src/port.ts new file mode 100644 index 0000000..600f0fc --- /dev/null +++ b/packages/storage/src/port.ts @@ -0,0 +1,19 @@ +// packages/storage/src/port.ts —— 存储端口(唯一抽象):生产 OSS 适配器(P5/G1) +// 与本包 MinIO 适配器实现同一接口;api(presign/verify)与 worker(variants put/get) +// 只依赖此端口,不感知后端差异。 +export interface UploadCredential { + mode: 'presigned'; + url: string; + method: 'PUT'; + headers: Record; + expiresAt: string; // ISO +} +export interface StoragePort { + presignPut(objectKey: string, bytes: number, contentType: string, ttlSeconds: number): Promise; + presignGet(objectKey: string, ttlSeconds: number): Promise; + head(objectKey: string): Promise<{ bytes: number; etag: string } | null>; + getStream(objectKey: string): Promise; + put(objectKey: string, body: Buffer, contentType: string): Promise<{ etag: string }>; + /** 复制到正式键;目标已存在时校验一致性并幂等返回(禁止覆盖语义)。 */ + copyIfAbsent(src: string, dest: string, expectedBytes: number): Promise<{ created: boolean; etag: string }>; +} diff --git a/packages/storage/test/load-env.ts b/packages/storage/test/load-env.ts new file mode 100644 index 0000000..0b6ce43 --- /dev/null +++ b/packages/storage/test/load-env.ts @@ -0,0 +1,16 @@ +// packages/storage/test/load-env.ts —— 与 services/api/test/load-env.ts 同约定: +// Jest 侧唯一 env 来源是仓库根 .env。本包嵌套层级与 api 不同(packages/storage/test +// vs services/api/test),故向上查找 pnpm-workspace.yaml 定位仓库根,不硬编码层数。 +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { config } from 'dotenv'; + +let dir = __dirname; +while (!existsSync(join(dir, 'pnpm-workspace.yaml'))) { + const parent = dirname(dir); + if (parent === dir) { + throw new Error('load-env: 未找到 pnpm-workspace.yaml,无法定位仓库根(.env)'); + } + dir = parent; +} +config({ path: join(dir, '.env') }); diff --git a/packages/storage/test/minio.adapter.spec.ts b/packages/storage/test/minio.adapter.spec.ts new file mode 100644 index 0000000..d554821 --- /dev/null +++ b/packages/storage/test/minio.adapter.spec.ts @@ -0,0 +1,93 @@ +// packages/storage/test/minio.adapter.spec.ts —— MinIO 真实集成测试(依赖 +// docker compose 的 photo-live-minio-1 与仓库根 .env 的 S3_* 配置)。 +// 每次运行使用唯一前缀 test/-/,避免历史残留对象干扰断言。 +import { randomBytes, randomUUID } from 'node:crypto'; +import { ErrorCodes } from '@photo/contracts'; +import { StorageError, createStorageFromEnv, type MinioStorageAdapter } from '../src/index'; + +const PREFIX = `test/${Date.now()}-${randomUUID()}`; + +describe('MinioStorageAdapter(真实 MinIO 集成)', () => { + let storage: MinioStorageAdapter; + + beforeAll(() => { + storage = createStorageFromEnv(); + }); + + it('presignPut: 客户端按凭证直接 PUT 后,head 返回精确字节数与 etag', async () => { + const key = `${PREFIX}/presign-put.bin`; + const body = randomBytes(2048); + const credential = await storage.presignPut(key, body.length, 'application/octet-stream', 300); + + expect(credential.mode).toBe('presigned'); + expect(credential.method).toBe('PUT'); + expect(credential.headers['Content-Type']).toBe('application/octet-stream'); + expect(new Date(credential.expiresAt).getTime()).toBeGreaterThan(Date.now()); + + const res = await fetch(credential.url, { method: credential.method, headers: credential.headers, body }); + expect(res.ok).toBe(true); + + const head = await storage.head(key); + expect(head).not.toBeNull(); + expect(head?.bytes).toBe(body.length); + expect(head?.etag).toBeTruthy(); + }); + + it('put + getStream: 写入后流式读回的字节与写入一致', async () => { + const key = `${PREFIX}/put-get.bin`; + const body = randomBytes(4096); + const { etag } = await storage.put(key, body, 'application/octet-stream'); + expect(etag).toBeTruthy(); + + const stream = await storage.getStream(key); + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(Buffer.from(chunk as Uint8Array)); + } + expect(Buffer.concat(chunks).equals(body)).toBe(true); + }); + + it('copyIfAbsent: 首次 created:true;同参数重复调用幂等 created:false 且 etag 不变', async () => { + const srcKey = `${PREFIX}/copy-src.bin`; + const destKey = `${PREFIX}/copy-dest.bin`; + const body = randomBytes(512); + await storage.put(srcKey, body, 'application/octet-stream'); + + const first = await storage.copyIfAbsent(srcKey, destKey, body.length); + expect(first.created).toBe(true); + expect(first.etag).toBeTruthy(); + + const second = await storage.copyIfAbsent(srcKey, destKey, body.length); + expect(second.created).toBe(false); + expect(second.etag).toBe(first.etag); + }); + + it('copyIfAbsent: dest 已存在但字节数不符 → StorageError(UPLOAD_VERIFY_FAILED)', async () => { + const srcKey = `${PREFIX}/mismatch-src.bin`; + const destKey = `${PREFIX}/mismatch-dest.bin`; + const srcBody = randomBytes(64); + await storage.put(srcKey, srcBody, 'application/octet-stream'); + // 预置一个不同大小的 dest 对象,制造核验失败 + await storage.put(destKey, randomBytes(128), 'application/octet-stream'); + + const promise = storage.copyIfAbsent(srcKey, destKey, srcBody.length); + await expect(promise).rejects.toBeInstanceOf(StorageError); + await expect(promise).rejects.toMatchObject({ code: ErrorCodes.UPLOAD_VERIFY_FAILED }); + }); + + it('head: 缺失键返回 null(不抛错)', async () => { + const head = await storage.head(`${PREFIX}/missing.bin`); + expect(head).toBeNull(); + }); + + it('presignGet: 预签名 URL 取回的字节与对象一致', async () => { + const key = `${PREFIX}/presign-get.bin`; + const body = randomBytes(1024); + await storage.put(key, body, 'application/octet-stream'); + + const url = await storage.presignGet(key, 300); + const res = await fetch(url); + expect(res.ok).toBe(true); + expect(Buffer.from(await res.arrayBuffer()).equals(body)).toBe(true); + }); +}); diff --git a/packages/storage/tsconfig.json b/packages/storage/tsconfig.json new file mode 100644 index 0000000..8e9c91c --- /dev/null +++ b/packages/storage/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "lib": ["ES2022"], + "module": "commonjs", + "moduleResolution": "node10", + "declaration": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df485b7..7fb760f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 7.13.0(typescript@5.9.3) ts-jest: specifier: ^29.2.0 - version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.6.0 version: 5.9.3 @@ -64,6 +64,37 @@ importers: specifier: ^5.6.0 version: 5.9.3 + packages/storage: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.700.0 + version: 3.1134.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.700.0 + version: 3.1134.0 + '@photo/contracts': + specifier: workspace:* + version: link:../contracts + devDependencies: + '@types/jest': + specifier: ^29.5.13 + version: 29.5.14 + '@types/node': + specifier: ^22.10.0 + version: 22.20.3 + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)) + ts-jest: + specifier: ^29.2.5 + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + services/api: dependencies: '@nestjs/common': @@ -120,7 +151,7 @@ importers: version: 7.2.2 ts-jest: specifier: ^29.2.5 - version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.20.3)(typescript@5.9.3) @@ -130,6 +161,82 @@ importers: packages: + '@aws-sdk/checksums@3.1001.0': + resolution: {integrity: sha512-6uTniZc87q+B5eXouGTl+7Tmc482rEeCcvxpsvREP8EfF0gvloRZ41UOA9sbSJlyy8TbqIBXb3kKfKarEArUQA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1134.0': + resolution: {integrity: sha512-dHgbuWMkladVNpR5gEQ7K9ub20w/hcf9IJXtMr+S3/rYpr6Z4gWUEX1seAPj00nAxJqtAaYvX3wdr2BVukZXXQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.978.0': + resolution: {integrity: sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.71': + resolution: {integrity: sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.73': + resolution: {integrity: sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.16': + resolution: {integrity: sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.78': + resolution: {integrity: sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.83': + resolution: {integrity: sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.71': + resolution: {integrity: sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.15': + resolution: {integrity: sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.77': + resolution: {integrity: sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.76': + resolution: {integrity: sha512-NfnTkVUTBKTBuBgqaapFK9r3YdkKt1b2oRvgLzZq91bwNKh6ZS0S7sEcheguttREaL4iyfs/xQnqD7Z7AsWSsA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.45': + resolution: {integrity: sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.1134.0': + resolution: {integrity: sha512-75DC3biOXXmSWqCN2hwJmhMPjDdgc1twjNJEknuWhY9M/E9eFIQufXkwOQx5raNTg6P/FbCgk9tp4y2SZ7vZFA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1129.0': + resolution: {integrity: sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -691,6 +798,30 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@smithy/core@3.34.1': + resolution: {integrity: sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.8.0': + resolution: {integrity: sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.12.1': + resolution: {integrity: sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.18.0': + resolution: {integrity: sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==} + engines: {node: '>=18.0.0'} + '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} @@ -991,6 +1122,9 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.21: resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} @@ -2928,6 +3062,180 @@ packages: snapshots: + '@aws-sdk/checksums@3.1001.0': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1134.0': + dependencies: + '@aws-sdk/checksums': 3.1001.0 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/credential-provider-node': 3.972.83 + '@aws-sdk/middleware-sdk-s3': 3.972.76 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.978.0': + dependencies: + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.34.1 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.18.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.71': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.73': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.16': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/credential-provider-env': 3.972.71 + '@aws-sdk/credential-provider-http': 3.972.73 + '@aws-sdk/credential-provider-login': 3.972.78 + '@aws-sdk/credential-provider-process': 3.972.71 + '@aws-sdk/credential-provider-sso': 3.973.15 + '@aws-sdk/credential-provider-web-identity': 3.972.77 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.78': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.83': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.71 + '@aws-sdk/credential-provider-http': 3.972.73 + '@aws-sdk/credential-provider-ini': 3.973.16 + '@aws-sdk/credential-provider-process': 3.972.71 + '@aws-sdk/credential-provider-sso': 3.973.15 + '@aws-sdk/credential-provider-web-identity': 3.972.77 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.71': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.15': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/token-providers': 3.1129.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.77': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.76': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.45': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.1134.0': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.46': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1129.0': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.5': + dependencies: + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.40': + dependencies: + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -3675,6 +3983,39 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/core@3.34.1': + dependencies: + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.8.0': + dependencies: + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.12.1': + dependencies: + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.3': + dependencies: + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + + '@smithy/types@4.18.0': + dependencies: + tslib: 2.8.1 + '@tokenizer/inflate@0.2.7': dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -4033,6 +4374,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.14.1: {} + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 @@ -5914,7 +6257,7 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.9.3): + ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0