diff --git a/packages/server/package.json b/packages/server/package.json index c3c1eb5..8a8b4c5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -10,6 +10,8 @@ "migrate": "node -e \"import('./db.js').then((m) => m.migrate().then(() => process.exit(0)))\"" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1111.0", + "@aws-sdk/s3-request-presigner": "^3.1111.0", "@fastify/cookie": "^11.0.2", "@fastify/cors": "^11.1.0", "@fastify/jwt": "^9.1.0", diff --git a/packages/server/script-sandbox.js b/packages/server/script-sandbox.js index 4b7c7c7..d02cdc1 100644 --- a/packages/server/script-sandbox.js +++ b/packages/server/script-sandbox.js @@ -15,6 +15,8 @@ import { getVariablePlain } from "./variables-store.js"; const hostRequire = createRequire(import.meta.url); const ALLOWED_MODULES = new Set([ + "@aws-sdk/client-s3", + "@aws-sdk/s3-request-presigner", "axios", "jsonata", "mustache", diff --git a/packages/server/scripts/s3.js b/packages/server/scripts/s3.js new file mode 100644 index 0000000..f152699 --- /dev/null +++ b/packages/server/scripts/s3.js @@ -0,0 +1,508 @@ +import { + CopyObjectCommand, + DeleteObjectCommand, + GetObjectCommand, + HeadObjectCommand, + ListObjectsV2Command, + PutObjectCommand, + S3Client, +} from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +const ACTIONS = new Set(["list", "read", "write", "delete", "stat", "presign", "copy"]); + +function passContext(ctx) { + if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) { + return { ...ctx.context }; + } + return {}; +} + +function mergeData(data) { + if (data != null && typeof data === "object" && !Array.isArray(data)) { + return { ...data }; + } + return {}; +} + +function resolveAction(ctx) { + const raw = ctx.config?.action ?? ctx.data?.action ?? "list"; + if (typeof raw !== "string" || raw.length === 0) { + throw new Error("s3: action must be a non-empty string"); + } + const action = raw.toLowerCase(); + if (!ACTIONS.has(action)) { + throw new Error(`s3: unsupported action "${raw}"`); + } + return action; +} + +function resolveBucket(ctx) { + const bucket = ctx.config?.bucket ?? ctx.data?.bucket; + if (typeof bucket !== "string" || bucket.length === 0) { + throw new Error("s3: bucket is required (ctx.config.bucket or ctx.data.bucket)"); + } + return bucket; +} + +function resolveKey(ctx, { required = false } = {}) { + const key = ctx.config?.key ?? ctx.data?.key; + if (key == null || key === "") { + if (required) throw new Error("s3: key is required for this action"); + return undefined; + } + if (typeof key !== "string") { + throw new Error("s3: key must be a string"); + } + return key; +} + +async function resolveSecretValue(secretName, label) { + if (typeof secretName !== "string" || secretName.length === 0) { + throw new Error(`s3: ${label} is required`); + } + return $secrets.reveal(await $secrets.get(secretName)); +} + +async function resolveCredentials(ctx) { + const accessKeyIdSecret = ctx.config?.accessKeyIdSecret; + const secretAccessKeySecret = ctx.config?.secretAccessKeySecret; + + if ( + typeof accessKeyIdSecret === "string" && + accessKeyIdSecret.length > 0 && + typeof secretAccessKeySecret === "string" && + secretAccessKeySecret.length > 0 + ) { + return { + accessKeyId: await resolveSecretValue(accessKeyIdSecret, "accessKeyIdSecret"), + secretAccessKey: await resolveSecretValue(secretAccessKeySecret, "secretAccessKeySecret"), + }; + } + + const accessKeyId = ctx.config?.accessKeyId ?? ctx.data?.accessKeyId; + const secretAccessKey = ctx.config?.secretAccessKey ?? ctx.data?.secretAccessKey; + if (typeof accessKeyId === "string" && typeof secretAccessKey === "string") { + return { accessKeyId, secretAccessKey }; + } + + throw new Error( + "s3: credentials are required (accessKeyIdSecret + secretAccessKeySecret, or accessKeyId + secretAccessKey)", + ); +} + +function createS3Client(ctx, credentials) { + const endpoint = ctx.config?.endpoint ?? ctx.data?.endpoint; + const region = + typeof ctx.config?.region === "string" && ctx.config.region.length > 0 + ? ctx.config.region + : typeof ctx.data?.region === "string" && ctx.data.region.length > 0 + ? ctx.data.region + : "us-east-1"; + + /** @type {import("@aws-sdk/client-s3").S3ClientConfig} */ + const options = { + region, + credentials, + }; + + if (typeof endpoint === "string" && endpoint.length > 0) { + options.endpoint = endpoint; + } + if (ctx.config?.forcePathStyle === true || ctx.data?.forcePathStyle === true) { + options.forcePathStyle = true; + } + + return new S3Client(options); +} + +function toIsoDate(value) { + if (value == null) return null; + const date = value instanceof Date ? value : new Date(value); + if (Number.isNaN(date.getTime())) return null; + return date.toISOString(); +} + +function normalizeListedObject(item) { + return { + key: item.Key ?? null, + size: typeof item.Size === "number" ? item.Size : null, + modified: toIsoDate(item.LastModified), + etag: item.ETag ?? null, + storageClass: item.StorageClass ?? null, + }; +} + +async function readObjectBody(body) { + if (body == null) return Buffer.alloc(0); + if (typeof body.transformToByteArray === "function") { + return Buffer.from(await body.transformToByteArray()); + } + /** @type {Buffer[]} */ + const chunks = []; + for await (const chunk of body) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); +} + +function resolveWriteBody(ctx) { + if (ctx.config != null && typeof ctx.config === "object" && "body" in ctx.config) { + return ctx.config.body; + } + if (ctx.data?.file != null) { + const file = ctx.data.file; + if (Buffer.isBuffer(file) || file instanceof Uint8Array) { + return Buffer.from(file); + } + } + if (ctx.data?.body != null) { + const body = ctx.data.body; + if (Buffer.isBuffer(body) || body instanceof Uint8Array) { + return Buffer.from(body); + } + if (typeof body === "string") { + return Buffer.from(body, "utf8"); + } + return Buffer.from(JSON.stringify(body), "utf8"); + } + throw new Error("s3: write requires ctx.config.body, ctx.data.body, or ctx.data.file"); +} + +async function s3(ctx) { + const action = resolveAction(ctx); + const bucket = resolveBucket(ctx); + const credentials = await resolveCredentials(ctx); + const client = createS3Client(ctx, credentials); + + log.info({ action, bucket }, "s3: starting action"); + + /** @type {Record} */ + let result = { action, bucket }; + + switch (action) { + case "list": { + const prefix = ctx.config?.prefix ?? ctx.data?.prefix ?? ""; + const delimiter = ctx.config?.delimiter ?? ctx.data?.delimiter; + const maxKeys = Number(ctx.config?.maxKeys ?? ctx.data?.maxKeys ?? 1000); + const response = await client.send( + new ListObjectsV2Command({ + Bucket: bucket, + Prefix: typeof prefix === "string" ? prefix : "", + Delimiter: typeof delimiter === "string" && delimiter.length > 0 ? delimiter : undefined, + MaxKeys: Number.isFinite(maxKeys) && maxKeys > 0 ? maxKeys : 1000, + }), + ); + const objects = (response.Contents ?? []).map(normalizeListedObject); + const prefixes = (response.CommonPrefixes ?? []) + .map((entry) => entry.Prefix) + .filter((value) => typeof value === "string"); + result = { + ...result, + prefix: typeof prefix === "string" ? prefix : "", + objects, + prefixes, + count: objects.length, + isTruncated: response.IsTruncated === true, + nextContinuationToken: response.NextContinuationToken ?? null, + }; + break; + } + case "read": { + const key = resolveKey(ctx, { required: true }); + const outputVar = + typeof ctx.config?.outputVar === "string" && ctx.config.outputVar.length > 0 + ? ctx.config.outputVar + : "file"; + const response = await client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + }), + ); + const file = await readObjectBody(response.Body); + const contentType = response.ContentType ?? "application/octet-stream"; + const encoding = ctx.config?.encoding ?? ctx.data?.encoding; + result = { + ...result, + key, + [outputVar]: file, + contentType, + contentLength: file.length, + etag: response.ETag ?? null, + lastModified: toIsoDate(response.LastModified), + }; + if (encoding === "utf8" || encoding === "text") { + result.text = file.toString("utf8"); + } else if (encoding === "base64") { + result.base64 = file.toString("base64"); + } + break; + } + case "write": { + const key = resolveKey(ctx, { required: true }); + const body = resolveWriteBody(ctx); + const contentType = + ctx.config?.contentType ?? + ctx.data?.contentType ?? + (typeof ctx.data?.body === "string" ? "text/plain; charset=utf-8" : "application/octet-stream"); + const response = await client.send( + new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: body, + ContentType: typeof contentType === "string" ? contentType : undefined, + }), + ); + result = { + ...result, + key, + etag: response.ETag ?? null, + contentLength: body.length, + written: true, + }; + break; + } + case "delete": { + const key = resolveKey(ctx, { required: true }); + await client.send( + new DeleteObjectCommand({ + Bucket: bucket, + Key: key, + }), + ); + result = { + ...result, + key, + deleted: true, + }; + break; + } + case "stat": { + const key = resolveKey(ctx, { required: true }); + const response = await client.send( + new HeadObjectCommand({ + Bucket: bucket, + Key: key, + }), + ); + result = { + ...result, + key, + contentType: response.ContentType ?? null, + contentLength: typeof response.ContentLength === "number" ? response.ContentLength : null, + etag: response.ETag ?? null, + lastModified: toIsoDate(response.LastModified), + metadata: response.Metadata ?? {}, + }; + break; + } + case "presign": { + const key = resolveKey(ctx, { required: true }); + const method = String(ctx.config?.presignMethod ?? ctx.data?.presignMethod ?? "get").toLowerCase(); + const expiresIn = Number(ctx.config?.expiresIn ?? ctx.data?.expiresIn ?? 3600); + const command = + method === "put" + ? new PutObjectCommand({ Bucket: bucket, Key: key }) + : new GetObjectCommand({ Bucket: bucket, Key: key }); + const url = await getSignedUrl(client, command, { + expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600, + }); + result = { + ...result, + key, + method, + expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600, + url, + }; + break; + } + case "copy": { + const key = resolveKey(ctx, { required: true }); + const sourceKey = ctx.config?.sourceKey ?? ctx.data?.sourceKey; + const sourceBucket = ctx.config?.sourceBucket ?? ctx.data?.sourceBucket ?? bucket; + if (typeof sourceKey !== "string" || sourceKey.length === 0) { + throw new Error("s3: copy requires sourceKey (ctx.config.sourceKey or ctx.data.sourceKey)"); + } + const response = await client.send( + new CopyObjectCommand({ + Bucket: bucket, + Key: key, + CopySource: `${sourceBucket}/${sourceKey}`, + }), + ); + result = { + ...result, + key, + sourceBucket, + sourceKey, + etag: response.CopyObjectResult?.ETag ?? null, + copied: true, + }; + break; + } + default: + throw new Error(`s3: unsupported action "${action}"`); + } + + log.info({ action, bucket, key: result.key ?? null }, "s3: action complete"); + return { + output: { ...mergeData(ctx.data), ...result }, + context: { ...passContext(ctx), ...result }, + }; +} + +s3.meta = { + description: "Access S3-compatible object storage (AWS S3, MinIO, Cloudflare R2, etc.)", + previewConfigKey: "action", + tags: ["S3", "storage"], + config: { + action: { + type: "string", + default: "list", + enum: ["list", "read", "write", "delete", "stat", "presign", "copy"], + description: "Operation to perform", + }, + endpoint: { + type: "string", + required: false, + description: "Custom S3 endpoint URL (required for MinIO and most non-AWS providers)", + }, + region: { + type: "string", + default: "us-east-1", + description: "AWS region (still required by many S3-compatible APIs)", + }, + bucket: { + type: "string", + required: true, + description: "Bucket name", + }, + key: { + type: "string", + required: false, + description: "Object key (required for read, write, delete, stat, presign, copy)", + }, + prefix: { + type: "string", + required: false, + description: "List only keys under this prefix", + }, + delimiter: { + type: "string", + required: false, + description: "List folder delimiter, usually /", + }, + maxKeys: { + type: "number", + default: 1000, + description: "Maximum objects returned by list", + }, + forcePathStyle: { + type: "boolean", + default: false, + description: "Use path-style URLs (often required for MinIO)", + }, + accessKeyIdSecret: { + type: "string", + required: false, + description: "Named secret for the access key id", + }, + secretAccessKeySecret: { + type: "string", + required: false, + description: "Named secret for the secret access key", + }, + accessKeyId: { + type: "string", + required: false, + description: "Plain access key id (prefer secrets in production)", + }, + secretAccessKey: { + type: "string", + required: false, + description: "Plain secret access key (prefer secrets in production)", + }, + contentType: { + type: "string", + required: false, + description: "Content-Type for write", + }, + body: { + type: "any", + required: false, + description: "Body for write when not passed via data", + }, + outputVar: { + type: "string", + default: "file", + description: "Output key for read action bytes", + }, + encoding: { + type: "string", + required: false, + enum: ["utf8", "text", "base64"], + description: "Optional read decoding helper (adds text or base64 field)", + }, + expiresIn: { + type: "number", + default: 3600, + description: "Presigned URL lifetime in seconds", + }, + presignMethod: { + type: "string", + default: "get", + enum: ["get", "put"], + description: "Presign a download (get) or upload (put) URL", + }, + sourceBucket: { + type: "string", + required: false, + description: "Source bucket for copy (defaults to bucket)", + }, + sourceKey: { + type: "string", + required: false, + description: "Source key for copy", + }, + }, + input: { + action: { type: "string", required: false }, + bucket: { type: "string", required: false }, + key: { type: "string", required: false }, + prefix: { type: "string", required: false }, + body: { type: "any", required: false, description: "Write payload" }, + file: { type: "buffer", required: false, description: "Binary write payload" }, + sourceKey: { type: "string", required: false }, + sourceBucket: { type: "string", required: false }, + }, + output: { + action: { type: "string" }, + bucket: { type: "string" }, + objects: { type: "array", required: false, description: "list results" }, + file: { type: "buffer", required: false, description: "read bytes (or outputVar)" }, + url: { type: "string", required: false, description: "presigned URL" }, + written: { type: "boolean", required: false }, + deleted: { type: "boolean", required: false }, + copied: { type: "boolean", required: false }, + }, + context: { + action: { type: "string" }, + bucket: { type: "string" }, + }, + example: { + data: {}, + config: { + action: "list", + endpoint: "https://minio.example.com", + region: "us-east-1", + bucket: "backups", + prefix: "daily/", + forcePathStyle: true, + accessKeyIdSecret: "minio_access_key", + secretAccessKeySecret: "minio_secret_key", + }, + }, +}; + +export default s3; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caa1251..cc69c89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: packages/server: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1111.0 + version: 3.1111.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.1111.0 + version: 3.1111.0 '@fastify/cookie': specifier: ^11.0.2 version: 11.1.2 @@ -132,6 +138,82 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@aws-sdk/checksums@3.1000.28': + resolution: {integrity: sha512-VCpnmyHQ1IH49ni3LXnQj7DPr7rmcJmzYeiCkYdCcfgNtkvOj38cdcL9lapBWoItZWFACJPFJlymqC7/gem3Gw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1111.0': + resolution: {integrity: sha512-VnLT6aSTN8tWl/NsXUysXNZor7wQBp9CRwufo7kt8cwGXvHLZ0S/cV1K9WFcREGboVYSo3NGQ3ZvU7LRidh2aQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.8': + resolution: {integrity: sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.69': + resolution: {integrity: sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.71': + resolution: {integrity: sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.14': + resolution: {integrity: sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.76': + resolution: {integrity: sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.80': + resolution: {integrity: sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.69': + resolution: {integrity: sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.13': + resolution: {integrity: sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.75': + resolution: {integrity: sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.74': + resolution: {integrity: sha512-2lzoV2z2QO5KJZYGOCnIZ1WVQgzMECvwuzr1xb034a++8QW4U4eGrmC2u4yg1xvNv4TLL/Uv5DLyuAiw0b9z7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.43': + resolution: {integrity: sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.1111.0': + resolution: {integrity: sha512-ACp/VtDTw6AjFW5Q3M59uAMrBbAHeQS0UBIOIgUROO98Z3uhpiy37k9RmvxbXYVugmTcdNTy4DPVQtLG8sDy6g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.45': + resolution: {integrity: sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1111.0': + resolution: {integrity: sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.4': + resolution: {integrity: sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.39': + resolution: {integrity: sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA==} + 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'} @@ -618,6 +700,30 @@ packages: cpu: [x64] os: [win32] + '@smithy/core@3.33.2': + resolution: {integrity: sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==} + 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.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.11.2': + resolution: {integrity: sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.2': + resolution: {integrity: sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} + engines: {node: '>=18.0.0'} + '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -903,6 +1009,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@5.0.9: resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} @@ -2038,6 +2147,180 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.3.0 + '@aws-sdk/checksums@3.1000.28': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1111.0': + dependencies: + '@aws-sdk/checksums': 3.1000.28 + '@aws-sdk/core': 3.977.8 + '@aws-sdk/credential-provider-node': 3.972.80 + '@aws-sdk/middleware-sdk-s3': 3.972.74 + '@aws-sdk/signature-v4-multi-region': 3.996.45 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.8': + dependencies: + '@aws-sdk/types': 3.974.4 + '@aws-sdk/xml-builder': 3.972.39 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.33.2 + '@smithy/signature-v4': 5.7.2 + '@smithy/types': 4.17.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.71': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.14': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/credential-provider-env': 3.972.69 + '@aws-sdk/credential-provider-http': 3.972.71 + '@aws-sdk/credential-provider-login': 3.972.76 + '@aws-sdk/credential-provider-process': 3.972.69 + '@aws-sdk/credential-provider-sso': 3.973.13 + '@aws-sdk/credential-provider-web-identity': 3.972.75 + '@aws-sdk/nested-clients': 3.997.43 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.76': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/nested-clients': 3.997.43 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.80': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.69 + '@aws-sdk/credential-provider-http': 3.972.71 + '@aws-sdk/credential-provider-ini': 3.973.14 + '@aws-sdk/credential-provider-process': 3.972.69 + '@aws-sdk/credential-provider-sso': 3.973.13 + '@aws-sdk/credential-provider-web-identity': 3.972.75 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.13': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/nested-clients': 3.997.43 + '@aws-sdk/token-providers': 3.1111.0 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.75': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/nested-clients': 3.997.43 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.74': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/signature-v4-multi-region': 3.996.45 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.43': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/signature-v4-multi-region': 3.996.45 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.1111.0': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/signature-v4-multi-region': 3.996.45 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.45': + dependencies: + '@aws-sdk/types': 3.974.4 + '@smithy/signature-v4': 5.7.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1111.0': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/nested-clients': 3.997.43 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.4': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.39': + dependencies: + '@smithy/types': 4.17.2 + 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 @@ -2447,6 +2730,39 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.4': optional: true + '@smithy/core@3.33.2': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 @@ -2749,6 +3065,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4