diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index 795601d5a6..ba56ad6cc0 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -337,8 +337,42 @@ }, "config": { "type": "object", - "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html", + "description": "The config for the S3 compatible storage provider.", "properties": { + "endpoint": { + "type": "string", + "description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://.r2.cloudflarestorage.com\"." + }, + "region": { + "type": "string", + "description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2." + }, + "forcePathStyle": { + "type": "boolean", + "description": "Whether to use path-style bucket addressing." + }, + "requestTimeoutMs": { + "type": "number", + "description": "Request timeout in milliseconds." + }, + "minPartSize": { + "type": "number", + "description": "Minimum multipart part size in bytes." + }, + "presign": { + "type": "object", + "description": "Presigned URL behavior configuration.", + "properties": { + "expiresInSeconds": { + "type": "number", + "description": "Expiration time in seconds for presigned URLs." + }, + "signContentTypeForPut": { + "type": "boolean", + "description": "Whether to sign Content-Type for presigned PUT." + } + } + }, "credentials": { "type": "object", "description": "The credentials for the s3 compatible storage provider.", @@ -348,6 +382,9 @@ }, "secretAccessKey": { "type": "string" + }, + "sessionToken": { + "type": "string" } } } @@ -369,8 +406,42 @@ }, "config": { "type": "object", - "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html", + "description": "The config for the S3 compatible storage provider.", "properties": { + "endpoint": { + "type": "string", + "description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://.r2.cloudflarestorage.com\"." + }, + "region": { + "type": "string", + "description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2." + }, + "forcePathStyle": { + "type": "boolean", + "description": "Whether to use path-style bucket addressing." + }, + "requestTimeoutMs": { + "type": "number", + "description": "Request timeout in milliseconds." + }, + "minPartSize": { + "type": "number", + "description": "Minimum multipart part size in bytes." + }, + "presign": { + "type": "object", + "description": "Presigned URL behavior configuration.", + "properties": { + "expiresInSeconds": { + "type": "number", + "description": "Expiration time in seconds for presigned URLs." + }, + "signContentTypeForPut": { + "type": "boolean", + "description": "Whether to sign Content-Type for presigned PUT." + } + } + }, "credentials": { "type": "object", "description": "The credentials for the s3 compatible storage provider.", @@ -380,6 +451,9 @@ }, "secretAccessKey": { "type": "string" + }, + "sessionToken": { + "type": "string" } } }, @@ -458,8 +532,42 @@ }, "config": { "type": "object", - "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html", + "description": "The config for the S3 compatible storage provider.", "properties": { + "endpoint": { + "type": "string", + "description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://.r2.cloudflarestorage.com\"." + }, + "region": { + "type": "string", + "description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2." + }, + "forcePathStyle": { + "type": "boolean", + "description": "Whether to use path-style bucket addressing." + }, + "requestTimeoutMs": { + "type": "number", + "description": "Request timeout in milliseconds." + }, + "minPartSize": { + "type": "number", + "description": "Minimum multipart part size in bytes." + }, + "presign": { + "type": "object", + "description": "Presigned URL behavior configuration.", + "properties": { + "expiresInSeconds": { + "type": "number", + "description": "Expiration time in seconds for presigned URLs." + }, + "signContentTypeForPut": { + "type": "boolean", + "description": "Whether to sign Content-Type for presigned PUT." + } + } + }, "credentials": { "type": "object", "description": "The credentials for the s3 compatible storage provider.", @@ -469,6 +577,9 @@ }, "secretAccessKey": { "type": "string" + }, + "sessionToken": { + "type": "string" } } } @@ -490,8 +601,42 @@ }, "config": { "type": "object", - "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html", + "description": "The config for the S3 compatible storage provider.", "properties": { + "endpoint": { + "type": "string", + "description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://.r2.cloudflarestorage.com\"." + }, + "region": { + "type": "string", + "description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2." + }, + "forcePathStyle": { + "type": "boolean", + "description": "Whether to use path-style bucket addressing." + }, + "requestTimeoutMs": { + "type": "number", + "description": "Request timeout in milliseconds." + }, + "minPartSize": { + "type": "number", + "description": "Minimum multipart part size in bytes." + }, + "presign": { + "type": "object", + "description": "Presigned URL behavior configuration.", + "properties": { + "expiresInSeconds": { + "type": "number", + "description": "Expiration time in seconds for presigned URLs." + }, + "signContentTypeForPut": { + "type": "boolean", + "description": "Whether to sign Content-Type for presigned PUT." + } + } + }, "credentials": { "type": "object", "description": "The credentials for the s3 compatible storage provider.", @@ -501,6 +646,9 @@ }, "secretAccessKey": { "type": "string" + }, + "sessionToken": { + "type": "string" } } }, @@ -941,8 +1089,42 @@ }, "config": { "type": "object", - "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html", + "description": "The config for the S3 compatible storage provider.", "properties": { + "endpoint": { + "type": "string", + "description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://.r2.cloudflarestorage.com\"." + }, + "region": { + "type": "string", + "description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2." + }, + "forcePathStyle": { + "type": "boolean", + "description": "Whether to use path-style bucket addressing." + }, + "requestTimeoutMs": { + "type": "number", + "description": "Request timeout in milliseconds." + }, + "minPartSize": { + "type": "number", + "description": "Minimum multipart part size in bytes." + }, + "presign": { + "type": "object", + "description": "Presigned URL behavior configuration.", + "properties": { + "expiresInSeconds": { + "type": "number", + "description": "Expiration time in seconds for presigned URLs." + }, + "signContentTypeForPut": { + "type": "boolean", + "description": "Whether to sign Content-Type for presigned PUT." + } + } + }, "credentials": { "type": "object", "description": "The credentials for the s3 compatible storage provider.", @@ -952,6 +1134,9 @@ }, "secretAccessKey": { "type": "string" + }, + "sessionToken": { + "type": "string" } } } @@ -973,8 +1158,42 @@ }, "config": { "type": "object", - "description": "The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html", + "description": "The config for the S3 compatible storage provider.", "properties": { + "endpoint": { + "type": "string", + "description": "The S3 compatible endpoint. Example: \"https://s3.us-east-1.amazonaws.com\" or \"https://.r2.cloudflarestorage.com\"." + }, + "region": { + "type": "string", + "description": "The region for the storage provider. Example: \"us-east-1\" or \"auto\" for R2." + }, + "forcePathStyle": { + "type": "boolean", + "description": "Whether to use path-style bucket addressing." + }, + "requestTimeoutMs": { + "type": "number", + "description": "Request timeout in milliseconds." + }, + "minPartSize": { + "type": "number", + "description": "Minimum multipart part size in bytes." + }, + "presign": { + "type": "object", + "description": "Presigned URL behavior configuration.", + "properties": { + "expiresInSeconds": { + "type": "number", + "description": "Expiration time in seconds for presigned URLs." + }, + "signContentTypeForPut": { + "type": "boolean", + "description": "Whether to sign Content-Type for presigned PUT." + } + } + }, "credentials": { "type": "object", "description": "The credentials for the s3 compatible storage provider.", @@ -984,6 +1203,9 @@ }, "secretAccessKey": { "type": "string" + }, + "sessionToken": { + "type": "string" } } }, diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index c90807448b..e7b94237c2 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -26,6 +26,7 @@ "postinstall": "prisma generate" }, "dependencies": { + "@affine/s3-compat": "workspace:*", "@affine/server-native": "workspace:*", "@ai-sdk/anthropic": "^2.0.54", "@ai-sdk/google": "^2.0.45", @@ -34,8 +35,6 @@ "@ai-sdk/openai-compatible": "^1.0.28", "@ai-sdk/perplexity": "^2.0.21", "@apollo/server": "^4.12.2", - "@aws-sdk/client-s3": "^3.948.0", - "@aws-sdk/s3-request-presigner": "^3.948.0", "@fal-ai/serverless-client": "^0.15.0", "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", "@google-cloud/opentelemetry-resource-util": "^3.0.0", diff --git a/packages/backend/server/src/__tests__/e2e/storage/r2-proxy.spec.ts b/packages/backend/server/src/__tests__/e2e/storage/r2-proxy.spec.ts index 18fb9a2863..799a32ee31 100644 --- a/packages/backend/server/src/__tests__/e2e/storage/r2-proxy.spec.ts +++ b/packages/backend/server/src/__tests__/e2e/storage/r2-proxy.spec.ts @@ -41,9 +41,7 @@ class MockR2Provider extends R2StorageProvider { super(config, bucket); } - destroy() { - this.client.destroy(); - } + destroy() {} // @ts-ignore expect override override async proxyPutObject( @@ -66,7 +64,7 @@ class MockR2Provider extends R2StorageProvider { body: any, options: { contentLength?: number } = {} ) { - const etag = `"etag-${partNumber}"`; + const etag = `etag-${partNumber}`; this.partCalls.push({ key, uploadId, @@ -322,7 +320,7 @@ e2e('should proxy multipart upload and return etag', async t => { .send(payload); t.is(res.status, 200); - t.is(res.get('etag'), '"etag-1"'); + t.is(res.get('etag'), 'etag-1'); const calls = getProvider().partCalls; t.is(calls.length, 1); @@ -356,7 +354,7 @@ e2e('should resume multipart upload and return uploaded parts', async t => { const init2 = await createBlobUpload(workspace.id, key, totalSize, 'bin'); t.is(init2.method, 'MULTIPART'); t.is(init2.uploadId, 'upload-id'); - t.deepEqual(init2.uploadedParts, [{ partNumber: 1, etag: '"etag-1"' }]); + t.deepEqual(init2.uploadedParts, [{ partNumber: 1, etag: 'etag-1' }]); t.is(getProvider().createMultipartCalls, 1); }); diff --git a/packages/backend/server/src/base/config/__tests__/config.spec.ts b/packages/backend/server/src/base/config/__tests__/config.spec.ts index 55185b4592..0ae9c88d1e 100644 --- a/packages/backend/server/src/base/config/__tests__/config.spec.ts +++ b/packages/backend/server/src/base/config/__tests__/config.spec.ts @@ -141,7 +141,7 @@ test('should override correctly', t => { config: { credentials: { accessKeyId: '1', - accessKeySecret: '1', + secretAccessKey: '1', }, }, }, @@ -169,7 +169,7 @@ test('should override correctly', t => { config: { credentials: { accessKeyId: '1', - accessKeySecret: '1', + secretAccessKey: '1', }, }, }); diff --git a/packages/backend/server/src/base/storage/__tests__/s3-list-parts.spec.ts b/packages/backend/server/src/base/storage/__tests__/s3-list-parts.spec.ts new file mode 100644 index 0000000000..407de50bc0 --- /dev/null +++ b/packages/backend/server/src/base/storage/__tests__/s3-list-parts.spec.ts @@ -0,0 +1,49 @@ +import { parseListPartsXml } from '@affine/s3-compat'; +import test from 'ava'; + +test('parseListPartsXml handles array parts and pagination', t => { + const xml = ` + + test + key + upload-id + 0 + 3 + true + + 1 + "etag-1" + + + 2 + etag-2 + +`; + + const result = parseListPartsXml(xml); + t.deepEqual(result.parts, [ + { partNumber: 1, etag: 'etag-1' }, + { partNumber: 2, etag: 'etag-2' }, + ]); + t.true(result.isTruncated); + t.is(result.nextPartNumberMarker, '3'); +}); + +test('parseListPartsXml handles single part', t => { + const xml = ` + + test + key + upload-id + false + + 5 + "etag-5" + +`; + + const result = parseListPartsXml(xml); + t.deepEqual(result.parts, [{ partNumber: 5, etag: 'etag-5' }]); + t.false(result.isTruncated); + t.is(result.nextPartNumberMarker, undefined); +}); diff --git a/packages/backend/server/src/base/storage/__tests__/s3.spec.ts b/packages/backend/server/src/base/storage/__tests__/s3.spec.ts index 8d789a73a7..f596784f14 100644 --- a/packages/backend/server/src/base/storage/__tests__/s3.spec.ts +++ b/packages/backend/server/src/base/storage/__tests__/s3.spec.ts @@ -4,7 +4,8 @@ import { S3StorageProvider } from '../providers/s3'; import { SIGNED_URL_EXPIRED } from '../providers/utils'; const config = { - region: 'auto', + region: 'us-east-1', + endpoint: 'https://s3.us-east-1.amazonaws.com', credentials: { accessKeyId: 'test', secretAccessKey: 'test', @@ -24,6 +25,8 @@ test('presignPut should return url and headers', async t => { t.truthy(result); t.true(result!.url.length > 0); t.true(result!.url.includes('X-Amz-Algorithm=AWS4-HMAC-SHA256')); + t.true(result!.url.includes('X-Amz-SignedHeaders=')); + t.true(result!.url.includes('content-type')); t.deepEqual(result!.headers, { 'Content-Type': 'text/plain' }); const now = Date.now(); t.true(result!.expiresAt.getTime() >= now + SIGNED_URL_EXPIRED * 1000 - 2000); @@ -41,12 +44,15 @@ test('presignUploadPart should return url', async t => { test('createMultipartUpload should return uploadId', async t => { const provider = createProvider(); - let receivedCommand: any; - const sendStub = async (command: any) => { - receivedCommand = command; - return { UploadId: 'upload-1' }; + let receivedKey: string | undefined; + let receivedMeta: any; + (provider as any).client = { + createMultipartUpload: async (key: string, meta: any) => { + receivedKey = key; + receivedMeta = meta; + return { uploadId: 'upload-1' }; + }, }; - (provider as any).client = { send: sendStub }; const now = Date.now(); const result = await provider.createMultipartUpload('key', { @@ -56,25 +62,29 @@ test('createMultipartUpload should return uploadId', async t => { t.is(result?.uploadId, 'upload-1'); t.true(result!.expiresAt.getTime() >= now + SIGNED_URL_EXPIRED * 1000 - 2000); t.true(result!.expiresAt.getTime() <= now + SIGNED_URL_EXPIRED * 1000 + 2000); - t.is(receivedCommand.input.Key, 'key'); - t.is(receivedCommand.input.ContentType, 'text/plain'); + t.is(receivedKey, 'key'); + t.is(receivedMeta.contentType, 'text/plain'); }); test('completeMultipartUpload should order parts', async t => { const provider = createProvider(); - let called = false; - const sendStub = async (command: any) => { - called = true; - t.deepEqual(command.input.MultipartUpload.Parts, [ - { ETag: 'a', PartNumber: 1 }, - { ETag: 'b', PartNumber: 2 }, - ]); + let receivedParts: any; + (provider as any).client = { + completeMultipartUpload: async ( + _key: string, + _uploadId: string, + parts: any + ) => { + receivedParts = parts; + }, }; - (provider as any).client = { send: sendStub }; await provider.completeMultipartUpload('key', 'upload-1', [ { partNumber: 2, etag: 'b' }, { partNumber: 1, etag: 'a' }, ]); - t.true(called); + t.deepEqual(receivedParts, [ + { partNumber: 1, etag: 'a' }, + { partNumber: 2, etag: 'b' }, + ]); }); diff --git a/packages/backend/server/src/base/storage/providers/index.ts b/packages/backend/server/src/base/storage/providers/index.ts index 6a9096d272..c8480e8b54 100644 --- a/packages/backend/server/src/base/storage/providers/index.ts +++ b/packages/backend/server/src/base/storage/providers/index.ts @@ -33,9 +33,44 @@ export type StorageProviderConfig = { bucket: string } & ( const S3ConfigSchema: JSONSchema = { type: 'object', - description: - 'The config for the s3 compatible storage provider. directly passed to aws-sdk client.\n@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html', + description: 'The config for the S3 compatible storage provider.', properties: { + endpoint: { + type: 'string', + description: + 'The S3 compatible endpoint. Example: "https://s3.us-east-1.amazonaws.com" or "https://.r2.cloudflarestorage.com".', + }, + region: { + type: 'string', + description: + 'The region for the storage provider. Example: "us-east-1" or "auto" for R2.', + }, + forcePathStyle: { + type: 'boolean', + description: 'Whether to use path-style bucket addressing.', + }, + requestTimeoutMs: { + type: 'number', + description: 'Request timeout in milliseconds.', + }, + minPartSize: { + type: 'number', + description: 'Minimum multipart part size in bytes.', + }, + presign: { + type: 'object', + description: 'Presigned URL behavior configuration.', + properties: { + expiresInSeconds: { + type: 'number', + description: 'Expiration time in seconds for presigned URLs.', + }, + signContentTypeForPut: { + type: 'boolean', + description: 'Whether to sign Content-Type for presigned PUT.', + }, + }, + }, credentials: { type: 'object', description: 'The credentials for the s3 compatible storage provider.', @@ -46,6 +81,9 @@ const S3ConfigSchema: JSONSchema = { secretAccessKey: { type: 'string', }, + sessionToken: { + type: 'string', + }, }, }, }, diff --git a/packages/backend/server/src/base/storage/providers/r2.ts b/packages/backend/server/src/base/storage/providers/r2.ts index 5b20643143..b7f08d0d4e 100644 --- a/packages/backend/server/src/base/storage/providers/r2.ts +++ b/packages/backend/server/src/base/storage/providers/r2.ts @@ -1,7 +1,6 @@ import assert from 'node:assert'; import { Readable } from 'node:stream'; -import { PutObjectCommand, UploadPartCommand } from '@aws-sdk/client-s3'; import { Logger } from '@nestjs/common'; import { @@ -39,9 +38,6 @@ export class R2StorageProvider extends S3StorageProvider { ...config, forcePathStyle: true, endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`, - // see https://github.com/aws/aws-sdk-js-v3/issues/6810 - requestChecksumCalculation: 'WHEN_REQUIRED', - responseChecksumValidation: 'WHEN_REQUIRED', }, bucket ); @@ -179,15 +175,10 @@ export class R2StorageProvider extends S3StorageProvider { body: Readable | Buffer | Uint8Array | string, options: { contentType?: string; contentLength?: number } = {} ) { - return this.client.send( - new PutObjectCommand({ - Bucket: this.bucket, - Key: key, - Body: body, - ContentType: options.contentType, - ContentLength: options.contentLength, - }) - ); + return this.client.putObject(key, body as any, { + contentType: options.contentType, + contentLength: options.contentLength, + }); } async proxyUploadPart( @@ -197,18 +188,15 @@ export class R2StorageProvider extends S3StorageProvider { body: Readable | Buffer | Uint8Array | string, options: { contentLength?: number } = {} ) { - const result = await this.client.send( - new UploadPartCommand({ - Bucket: this.bucket, - Key: key, - UploadId: uploadId, - PartNumber: partNumber, - Body: body, - ContentLength: options.contentLength, - }) + const result = await this.client.uploadPart( + key, + uploadId, + partNumber, + body as any, + { contentLength: options.contentLength } ); - return result.ETag; + return result.etag; } override async get( diff --git a/packages/backend/server/src/base/storage/providers/s3.ts b/packages/backend/server/src/base/storage/providers/s3.ts index 4531fe47d2..1bafcb395c 100644 --- a/packages/backend/server/src/base/storage/providers/s3.ts +++ b/packages/backend/server/src/base/storage/providers/s3.ts @@ -1,24 +1,12 @@ /* oxlint-disable @typescript-eslint/no-non-null-assertion */ import { Readable } from 'node:stream'; -import { - AbortMultipartUploadCommand, - CompleteMultipartUploadCommand, - CreateMultipartUploadCommand, - DeleteObjectCommand, - GetObjectCommand, - HeadObjectCommand, - ListObjectsV2Command, - ListPartsCommand, - NoSuchKey, - NoSuchUpload, - NotFound, - PutObjectCommand, - S3Client, - S3ClientConfig, - UploadPartCommand, -} from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import type { + S3CompatClient, + S3CompatConfig, + S3CompatCredentials, +} from '@affine/s3-compat'; +import { createS3CompatClient } from '@affine/s3-compat'; import { Logger } from '@nestjs/common'; import { @@ -33,30 +21,55 @@ import { } from './provider'; import { autoMetadata, SIGNED_URL_EXPIRED, toBuffer } from './utils'; -export interface S3StorageConfig extends S3ClientConfig { +export interface S3StorageConfig { + endpoint?: string; + region: string; + credentials: S3CompatCredentials; + forcePathStyle?: boolean; + requestTimeoutMs?: number; + minPartSize?: number; + presign?: { + expiresInSeconds?: number; + signContentTypeForPut?: boolean; + }; usePresignedURL?: { enabled: boolean; }; } +function resolveEndpoint(config: S3StorageConfig) { + if (config.endpoint) { + return config.endpoint; + } + if (config.region === 'us-east-1') { + return 'https://s3.amazonaws.com'; + } + return `https://s3.${config.region}.amazonaws.com`; +} + export class S3StorageProvider implements StorageProvider { protected logger: Logger; - protected client: S3Client; + protected client: S3CompatClient; private readonly usePresignedURL: boolean; constructor( config: S3StorageConfig, public readonly bucket: string ) { - const { usePresignedURL, ...clientConfig } = config; - this.client = new S3Client({ - region: 'auto', - // s3 client uses keep-alive by default to accelerate requests, and max requests queue is 50. - // If some of them are long holding or dead without response, the whole queue will block. - // By default no timeout is set for requests or connections, so we set them here. - requestHandler: { requestTimeout: 60_000, connectionTimeout: 10_000 }, + const { usePresignedURL, presign, credentials, ...clientConfig } = config; + + const compatConfig: S3CompatConfig = { ...clientConfig, - }); + endpoint: resolveEndpoint(config), + bucket, + requestTimeoutMs: clientConfig.requestTimeoutMs ?? 60_000, + presign: { + expiresInSeconds: presign?.expiresInSeconds ?? SIGNED_URL_EXPIRED, + signContentTypeForPut: presign?.signContentTypeForPut ?? true, + }, + }; + + this.client = createS3CompatClient(compatConfig, credentials); this.usePresignedURL = usePresignedURL?.enabled ?? false; this.logger = new Logger(`${S3StorageProvider.name}:${bucket}`); } @@ -71,19 +84,10 @@ export class S3StorageProvider implements StorageProvider { metadata = autoMetadata(blob, metadata); try { - await this.client.send( - new PutObjectCommand({ - Bucket: this.bucket, - Key: key, - Body: blob, - - // metadata - ContentType: metadata.contentType, - ContentLength: metadata.contentLength, - // TODO(@forehalo): Cloudflare doesn't support CRC32, use md5 instead later. - // ChecksumCRC32: metadata.checksumCRC32, - }) - ); + await this.client.putObject(key, blob, { + contentType: metadata.contentType, + contentLength: metadata.contentLength, + }); this.logger.verbose(`Object \`${key}\` put`); } catch (e) { @@ -104,20 +108,12 @@ export class S3StorageProvider implements StorageProvider { ): Promise { try { const contentType = metadata.contentType ?? 'application/octet-stream'; - const url = await getSignedUrl( - this.client, - new PutObjectCommand({ - Bucket: this.bucket, - Key: key, - ContentType: contentType, - }), - { expiresIn: SIGNED_URL_EXPIRED } - ); + const result = await this.client.presignPutObject(key, { contentType }); return { - url, - headers: { 'Content-Type': contentType }, - expiresAt: new Date(Date.now() + SIGNED_URL_EXPIRED * 1000), + url: result.url, + headers: result.headers, + expiresAt: result.expiresAt, }; } catch (e) { this.logger.error( @@ -137,20 +133,16 @@ export class S3StorageProvider implements StorageProvider { ): Promise { try { const contentType = metadata.contentType ?? 'application/octet-stream'; - const response = await this.client.send( - new CreateMultipartUploadCommand({ - Bucket: this.bucket, - Key: key, - ContentType: contentType, - }) - ); + const response = await this.client.createMultipartUpload(key, { + contentType, + }); - if (!response.UploadId) { + if (!response.uploadId) { return; } return { - uploadId: response.UploadId, + uploadId: response.uploadId, expiresAt: new Date(Date.now() + SIGNED_URL_EXPIRED * 1000), }; } catch (e) { @@ -171,20 +163,15 @@ export class S3StorageProvider implements StorageProvider { partNumber: number ): Promise { try { - const url = await getSignedUrl( - this.client, - new UploadPartCommand({ - Bucket: this.bucket, - Key: key, - UploadId: uploadId, - PartNumber: partNumber, - }), - { expiresIn: SIGNED_URL_EXPIRED } + const result = await this.client.presignUploadPart( + key, + uploadId, + partNumber ); return { - url, - expiresAt: new Date(Date.now() + SIGNED_URL_EXPIRED * 1000), + url: result.url, + expiresAt: result.expiresAt, }; } catch (e) { this.logger.error( @@ -198,47 +185,9 @@ export class S3StorageProvider implements StorageProvider { key: string, uploadId: string ): Promise { - const parts: MultipartUploadPart[] = []; - let partNumberMarker: string | undefined; - try { - // ListParts is paginated by part number marker - // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html - // R2 follows S3 semantics here. - while (true) { - const response = await this.client.send( - new ListPartsCommand({ - Bucket: this.bucket, - Key: key, - UploadId: uploadId, - PartNumberMarker: partNumberMarker, - }) - ); - - for (const part of response.Parts ?? []) { - if (!part.PartNumber || !part.ETag) { - continue; - } - parts.push({ partNumber: part.PartNumber, etag: part.ETag }); - } - - if (!response.IsTruncated) { - break; - } - - if (response.NextPartNumberMarker === undefined) { - break; - } - - partNumberMarker = response.NextPartNumberMarker; - } - - return parts; + return await this.client.listParts(key, uploadId); } catch (e) { - // the upload may have been aborted/expired by provider lifecycle rules - if (e instanceof NoSuchUpload || e instanceof NotFound) { - return undefined; - } this.logger.error(`Failed to list multipart upload parts for \`${key}\``); throw e; } @@ -254,19 +203,7 @@ export class S3StorageProvider implements StorageProvider { (left, right) => left.partNumber - right.partNumber ); - await this.client.send( - new CompleteMultipartUploadCommand({ - Bucket: this.bucket, - Key: key, - UploadId: uploadId, - MultipartUpload: { - Parts: orderedParts.map(part => ({ - ETag: part.etag, - PartNumber: part.partNumber, - })), - }, - }) - ); + await this.client.completeMultipartUpload(key, uploadId, orderedParts); } catch (e) { this.logger.error(`Failed to complete multipart upload for \`${key}\``); throw e; @@ -275,13 +212,7 @@ export class S3StorageProvider implements StorageProvider { async abortMultipartUpload(key: string, uploadId: string): Promise { try { - await this.client.send( - new AbortMultipartUploadCommand({ - Bucket: this.bucket, - Key: key, - UploadId: uploadId, - }) - ); + await this.client.abortMultipartUpload(key, uploadId); } catch (e) { this.logger.error(`Failed to abort multipart upload for \`${key}\``); throw e; @@ -290,25 +221,19 @@ export class S3StorageProvider implements StorageProvider { async head(key: string) { try { - const obj = await this.client.send( - new HeadObjectCommand({ - Bucket: this.bucket, - Key: key, - }) - ); - - return { - contentType: obj.ContentType!, - contentLength: obj.ContentLength!, - lastModified: obj.LastModified!, - checksumCRC32: obj.ChecksumCRC32, - }; - } catch (e) { - // 404 - if (e instanceof NoSuchKey || e instanceof NotFound) { + const obj = await this.client.headObject(key); + if (!obj) { this.logger.verbose(`Object \`${key}\` not found`); return undefined; } + + return { + contentType: obj.contentType ?? 'application/octet-stream', + contentLength: obj.contentLength ?? 0, + lastModified: obj.lastModified ?? new Date(0), + checksumCRC32: obj.checksumCRC32, + }; + } catch (e) { this.logger.error(`Failed to head object \`${key}\``); throw e; } @@ -323,25 +248,13 @@ export class S3StorageProvider implements StorageProvider { redirectUrl?: string; }> { try { - const command = new GetObjectCommand({ - Bucket: this.bucket, - Key: key, - }); - if (this.usePresignedURL && signedUrl) { const metadata = await this.head(key); if (metadata) { - const url = await getSignedUrl( - this.client, - new GetObjectCommand({ - Bucket: this.bucket, - Key: key, - }), - { expiresIn: SIGNED_URL_EXPIRED } - ); + const result = await this.client.presignGetObject(key); return { - redirectUrl: url, + redirectUrl: result.url, metadata, }; } @@ -350,68 +263,41 @@ export class S3StorageProvider implements StorageProvider { return {}; } - const obj = await this.client.send(command); - - if (!obj.Body) { + const obj = await this.client.getObjectResponse(key); + if (!obj || !obj.body) { this.logger.verbose(`Object \`${key}\` not found`); return {}; } + const contentType = obj.headers.get('content-type') ?? undefined; + const contentLengthHeader = obj.headers.get('content-length'); + const contentLength = contentLengthHeader + ? Number(contentLengthHeader) + : undefined; + const lastModifiedHeader = obj.headers.get('last-modified'); + const lastModified = lastModifiedHeader + ? new Date(lastModifiedHeader) + : undefined; + this.logger.verbose(`Read object \`${key}\``); return { - // @ts-expect-errors ignore browser response type `Blob` - body: obj.Body, + body: Readable.fromWeb(obj.body as any), metadata: { - // always set when putting object - contentType: obj.ContentType ?? 'application/octet-stream', - contentLength: obj.ContentLength!, - lastModified: obj.LastModified!, - checksumCRC32: obj.ChecksumCRC32, + contentType: contentType ?? 'application/octet-stream', + contentLength: contentLength ?? 0, + lastModified: lastModified ?? new Date(0), + checksumCRC32: obj.headers.get('x-amz-checksum-crc32') ?? undefined, }, }; } catch (e) { - // 404 - if (e instanceof NoSuchKey) { - this.logger.verbose(`Object \`${key}\` not found`); - return {}; - } this.logger.error(`Failed to read object \`${key}\``); throw e; } } async list(prefix?: string): Promise { - // continuationToken should be `string | undefined`, - // but TypeScript will fail on type infer in the code below. - // Seems to be a bug in TypeScript - let continuationToken: any = undefined; - let hasMore = true; - let result: ListObjectsMetadata[] = []; - try { - while (hasMore) { - const listResult = await this.client.send( - new ListObjectsV2Command({ - Bucket: this.bucket, - Prefix: prefix, - ContinuationToken: continuationToken, - }) - ); - - if (listResult.Contents?.length) { - result = result.concat( - listResult.Contents.map(r => ({ - key: r.Key!, - lastModified: r.LastModified!, - contentLength: r.Size!, - })) - ); - } - - // has more items not listed - hasMore = !!listResult.IsTruncated; - continuationToken = listResult.NextContinuationToken; - } + const result = await this.client.listObjectsV2(prefix); this.logger.verbose( `List ${result.length} objects with prefix \`${prefix}\`` @@ -425,12 +311,7 @@ export class S3StorageProvider implements StorageProvider { async delete(key: string): Promise { try { - await this.client.send( - new DeleteObjectCommand({ - Bucket: this.bucket, - Key: key, - }) - ); + await this.client.deleteObject(key); this.logger.verbose(`Deleted object \`${key}\``); } catch (e) { diff --git a/packages/backend/server/src/core/sync/gateway.ts b/packages/backend/server/src/core/sync/gateway.ts index d789ac58e4..b312241280 100644 --- a/packages/backend/server/src/core/sync/gateway.ts +++ b/packages/backend/server/src/core/sync/gateway.ts @@ -23,6 +23,7 @@ import { SpaceAccessDenied, } from '../../base'; import { Models } from '../../models'; +import { mergeUpdatesInApplyWay } from '../../native'; import { CurrentUser } from '../auth'; import { DocReader, @@ -48,8 +49,9 @@ type EventResponse = Data extends never data: Data; }; -// 019 only receives space:broadcast-doc-updates and send space:push-doc-updates -// 020 only receives space:broadcast-doc-update and send space:push-doc-update +// sync-019: legacy 0.19.x clients (broadcast-doc-updates/push-doc-updates). +// Remove after 2026-06-30 once metrics show 0 usage for 30 days. +// 020+: receives space:broadcast-doc-updates (batch) and sends space:push-doc-update. type RoomType = 'sync' | `${string}:awareness` | 'sync-019'; function Room( @@ -105,6 +107,16 @@ interface PushDocUpdateMessage { update: string; } +interface BroadcastDocUpdatesMessage { + spaceType: SpaceType; + spaceId: string; + docId: string; + updates: string[]; + timestamp: number; + editor?: string; + compressed?: boolean; +} + interface LoadDocMessage { spaceType: SpaceType; spaceId: string; @@ -157,6 +169,62 @@ export class SpaceSyncGateway private readonly models: Models ) {} + private encodeUpdates(updates: Uint8Array[]) { + return updates.map(update => Buffer.from(update).toString('base64')); + } + + private buildBroadcastPayload( + spaceType: SpaceType, + spaceId: string, + docId: string, + updates: Uint8Array[], + timestamp: number, + editor?: string + ): BroadcastDocUpdatesMessage { + const encodedUpdates = this.encodeUpdates(updates); + if (updates.length <= 1) { + return { + spaceType, + spaceId, + docId, + updates: encodedUpdates, + timestamp, + editor, + compressed: false, + }; + } + + try { + const merged = mergeUpdatesInApplyWay( + updates.map(update => Buffer.from(update)) + ); + metrics.socketio.counter('doc_updates_compressed').add(1); + return { + spaceType, + spaceId, + docId, + updates: [Buffer.from(merged).toString('base64')], + timestamp, + editor, + compressed: true, + }; + } catch (error) { + this.logger.warn( + 'Failed to merge updates for broadcast, falling back to batch', + error as Error + ); + return { + spaceType, + spaceId, + docId, + updates: encodedUpdates, + timestamp, + editor, + compressed: false, + }; + } + } + handleConnection() { this.connectionCount++; this.logger.debug(`New connection, total: ${this.connectionCount}`); @@ -184,9 +252,7 @@ export class SpaceSyncGateway return; } - const encodedUpdates = updates.map(update => - Buffer.from(update).toString('base64') - ); + const encodedUpdates = this.encodeUpdates(updates); this.server .to(Room(spaceId, 'sync-019')) @@ -196,19 +262,27 @@ export class SpaceSyncGateway docId, updates: encodedUpdates, timestamp, - }); - - const room = `${spaceType}:${Room(spaceId)}`; - encodedUpdates.forEach(update => { - this.server.to(room).emit('space:broadcast-doc-update', { - spaceType, - spaceId, - docId, - update, - timestamp, editor, }); - }); + metrics.socketio + .counter('sync_019_broadcast') + .add(encodedUpdates.length, { event: 'doc_updates_pushed' }); + + const room = `${spaceType}:${Room(spaceId)}`; + const payload = this.buildBroadcastPayload( + spaceType as SpaceType, + spaceId, + docId, + updates, + timestamp, + editor + ); + this.server.to(room).emit('space:broadcast-doc-updates', payload); + metrics.socketio + .counter('doc_updates_broadcast') + .add(payload.updates.length, { + mode: payload.compressed ? 'compressed' : 'batch', + }); } selectAdapter(client: Socket, spaceType: SpaceType): SyncSocketAdapter { @@ -330,19 +404,35 @@ export class SpaceSyncGateway user.id ); + metrics.socketio + .counter('sync_019_event') + .add(1, { event: 'push-doc-updates' }); + // broadcast to 0.19.x clients - client - .to(Room(spaceId, 'sync-019')) - .emit('space:broadcast-doc-updates', { ...message, timestamp }); + client.to(Room(spaceId, 'sync-019')).emit('space:broadcast-doc-updates', { + ...message, + timestamp, + editor: user.id, + }); // broadcast to new clients - updates.forEach(update => { - client.to(adapter.room(spaceId)).emit('space:broadcast-doc-update', { - ...message, - update, - timestamp, + const decodedUpdates = updates.map(update => Buffer.from(update, 'base64')); + const payload = this.buildBroadcastPayload( + spaceType, + spaceId, + docId, + decodedUpdates, + timestamp, + user.id + ); + client + .to(adapter.room(spaceId)) + .emit('space:broadcast-doc-updates', payload); + metrics.socketio + .counter('doc_updates_broadcast') + .add(payload.updates.length, { + mode: payload.compressed ? 'compressed' : 'batch', }); - }); return { data: { @@ -378,16 +468,25 @@ export class SpaceSyncGateway docId, updates: [update], timestamp, + editor: user.id, }); - client.to(adapter.room(spaceId)).emit('space:broadcast-doc-update', { + const payload = this.buildBroadcastPayload( spaceType, spaceId, docId, - update, + [Buffer.from(update, 'base64')], timestamp, - editor: user.id, - }); + user.id + ); + client + .to(adapter.room(spaceId)) + .emit('space:broadcast-doc-updates', payload); + metrics.socketio + .counter('doc_updates_broadcast') + .add(payload.updates.length, { + mode: payload.compressed ? 'compressed' : 'batch', + }); return { data: { diff --git a/packages/backend/server/tsconfig.json b/packages/backend/server/tsconfig.json index 6997a939f7..7c1c612283 100644 --- a/packages/backend/server/tsconfig.json +++ b/packages/backend/server/tsconfig.json @@ -12,6 +12,7 @@ }, "include": ["./src"], "references": [ + { "path": "../../common/s3-compat" }, { "path": "../native" }, { "path": "../../../tools/cli" }, { "path": "../../../tools/utils" }, diff --git a/packages/common/nbstore/src/__tests__/base64.bench.ts b/packages/common/nbstore/src/__tests__/base64.bench.ts new file mode 100644 index 0000000000..483db83e92 --- /dev/null +++ b/packages/common/nbstore/src/__tests__/base64.bench.ts @@ -0,0 +1,23 @@ +import { bench, describe } from 'vitest'; + +import { base64ToUint8Array, uint8ArrayToBase64 } from '../impls/cloud/socket'; + +const data = new Uint8Array(1024 * 256); +for (let i = 0; i < data.length; i++) { + data[i] = i % 251; +} +let encoded = ''; + +await uint8ArrayToBase64(data).then(result => { + encoded = result; +}); + +describe('base64 helpers', () => { + bench('encode Uint8Array to base64', async () => { + await uint8ArrayToBase64(data); + }); + + bench('decode base64 to Uint8Array', () => { + base64ToUint8Array(encoded); + }); +}); diff --git a/packages/common/nbstore/src/__tests__/base64.spec.ts b/packages/common/nbstore/src/__tests__/base64.spec.ts new file mode 100644 index 0000000000..0a062b7acf --- /dev/null +++ b/packages/common/nbstore/src/__tests__/base64.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from 'vitest'; + +import { base64ToUint8Array, uint8ArrayToBase64 } from '../impls/cloud/socket'; + +function makeSample(size: number) { + const data = new Uint8Array(size); + for (let i = 0; i < size; i++) { + data[i] = i % 251; + } + return data; +} + +describe('base64 helpers', () => { + test('roundtrip preserves data', async () => { + const input = makeSample(1024); + const encoded = await uint8ArrayToBase64(input); + const decoded = base64ToUint8Array(encoded); + expect(decoded).toEqual(input); + }); + + test('handles large payloads', async () => { + const input = makeSample(256 * 1024); + const encoded = await uint8ArrayToBase64(input); + const decoded = base64ToUint8Array(encoded); + expect(decoded).toEqual(input); + }); +}); diff --git a/packages/common/nbstore/src/__tests__/cloud-doc-updates.spec.ts b/packages/common/nbstore/src/__tests__/cloud-doc-updates.spec.ts new file mode 100644 index 0000000000..71816d2d84 --- /dev/null +++ b/packages/common/nbstore/src/__tests__/cloud-doc-updates.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from 'vitest'; + +import { CloudDocStorage } from '../impls/cloud/doc'; + +const base64UpdateA = 'AQID'; +const base64UpdateB = 'BAUG'; + +describe('CloudDocStorage broadcast updates', () => { + test('emits updates from batch payload', () => { + const storage = new CloudDocStorage({ + id: 'space-1', + serverBaseUrl: 'http://localhost', + isSelfHosted: true, + type: 'workspace', + readonlyMode: true, + }); + + (storage as any).connection.idConverter = { + oldIdToNewId: (id: string) => id, + newIdToOldId: (id: string) => id, + }; + + const received: Uint8Array[] = []; + storage.subscribeDocUpdate(update => { + received.push(update.bin); + }); + + storage.onServerUpdates({ + spaceType: 'workspace', + spaceId: 'space-1', + docId: 'doc-1', + updates: [base64UpdateA, base64UpdateB], + timestamp: Date.now(), + }); + + expect(received).toEqual([ + new Uint8Array([1, 2, 3]), + new Uint8Array([4, 5, 6]), + ]); + }); +}); diff --git a/packages/common/nbstore/src/impls/cloud/doc.ts b/packages/common/nbstore/src/impls/cloud/doc.ts index c2fe4a06d7..1bb13a8b48 100644 --- a/packages/common/nbstore/src/impls/cloud/doc.ts +++ b/packages/common/nbstore/src/impls/cloud/doc.ts @@ -38,12 +38,32 @@ export class CloudDocStorage extends DocStorageBase { onServerUpdate: ServerEventsMap['space:broadcast-doc-update'] = message => { if ( - this.spaceType === message.spaceType && - this.spaceId === message.spaceId + this.spaceType !== message.spaceType || + this.spaceId !== message.spaceId ) { + return; + } + + this.emit('update', { + docId: this.idConverter.oldIdToNewId(message.docId), + bin: base64ToUint8Array(message.update), + timestamp: new Date(message.timestamp), + editor: message.editor, + }); + }; + + onServerUpdates: ServerEventsMap['space:broadcast-doc-updates'] = message => { + if ( + this.spaceType !== message.spaceType || + this.spaceId !== message.spaceId + ) { + return; + } + + for (const update of message.updates) { this.emit('update', { docId: this.idConverter.oldIdToNewId(message.docId), - bin: base64ToUint8Array(message.update), + bin: base64ToUint8Array(update), timestamp: new Date(message.timestamp), editor: message.editor, }); @@ -52,7 +72,8 @@ export class CloudDocStorage extends DocStorageBase { readonly connection = new CloudDocStorageConnection( this.options, - this.onServerUpdate + this.onServerUpdate, + this.onServerUpdates ); override async getDocSnapshot(docId: string) { @@ -184,7 +205,8 @@ export class CloudDocStorage extends DocStorageBase { class CloudDocStorageConnection extends SocketConnection { constructor( private readonly options: CloudDocStorageOptions, - private readonly onServerUpdate: ServerEventsMap['space:broadcast-doc-update'] + private readonly onServerUpdate: ServerEventsMap['space:broadcast-doc-update'], + private readonly onServerUpdates: ServerEventsMap['space:broadcast-doc-updates'] ) { super(options.serverBaseUrl, options.isSelfHosted); } @@ -210,6 +232,7 @@ class CloudDocStorageConnection extends SocketConnection { } socket.on('space:broadcast-doc-update', this.onServerUpdate); + socket.on('space:broadcast-doc-updates', this.onServerUpdates); return { socket, disconnect }; } catch (e) { @@ -230,6 +253,7 @@ class CloudDocStorageConnection extends SocketConnection { spaceId: this.options.id, }); socket.off('space:broadcast-doc-update', this.onServerUpdate); + socket.off('space:broadcast-doc-updates', this.onServerUpdates); super.doDisconnect({ socket, disconnect }); } diff --git a/packages/common/nbstore/src/impls/cloud/socket.ts b/packages/common/nbstore/src/impls/cloud/socket.ts index 37de029545..122361eb2d 100644 --- a/packages/common/nbstore/src/impls/cloud/socket.ts +++ b/packages/common/nbstore/src/impls/cloud/socket.ts @@ -30,6 +30,15 @@ interface ServerEvents { timestamp: number; editor: string; }; + 'space:broadcast-doc-updates': { + spaceType: string; + spaceId: string; + docId: string; + updates: string[]; + timestamp: number; + editor?: string; + compressed?: boolean; + }; 'space:collect-awareness': { spaceType: string; @@ -124,33 +133,42 @@ export type ClientEventsMap = { export type Socket = SocketIO; -export function uint8ArrayToBase64(array: Uint8Array): Promise { - return new Promise(resolve => { - // Create a blob from the Uint8Array - const blob = new Blob([array]); +type BufferConstructorLike = { + from( + data: Uint8Array | string, + encoding?: string + ): Uint8Array & { + toString(encoding: string): string; + }; +}; - const reader = new FileReader(); - reader.onload = function () { - const dataUrl = reader.result as string | null; - if (!dataUrl) { - resolve(''); - return; - } - // The result includes the `data:` URL prefix and the MIME type. We only want the Base64 data - const base64 = dataUrl.split(',')[1]; - resolve(base64); - }; +const BufferCtor = (globalThis as { Buffer?: BufferConstructorLike }).Buffer; +const CHUNK_SIZE = 0x8000; - reader.readAsDataURL(blob); - }); +export async function uint8ArrayToBase64(array: Uint8Array): Promise { + if (BufferCtor) { + return BufferCtor.from(array).toString('base64'); + } + + let binary = ''; + for (let i = 0; i < array.length; i += CHUNK_SIZE) { + const chunk = array.subarray(i, i + CHUNK_SIZE); + binary += String.fromCharCode(...chunk); + } + return btoa(binary); } export function base64ToUint8Array(base64: string) { + if (BufferCtor) { + return new Uint8Array(BufferCtor.from(base64, 'base64')); + } + const binaryString = atob(base64); - const binaryArray = [...binaryString].map(function (char) { - return char.charCodeAt(0); - }); - return new Uint8Array(binaryArray); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; } let authMethod: diff --git a/packages/common/s3-compat/package.json b/packages/common/s3-compat/package.json new file mode 100644 index 0000000000..a4aa447135 --- /dev/null +++ b/packages/common/s3-compat/package.json @@ -0,0 +1,18 @@ +{ + "name": "@affine/s3-compat", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "aws4": "^1.13.2", + "fast-xml-parser": "^5.3.4", + "s3mini": "^0.9.1" + }, + "devDependencies": { + "@types/aws4": "^1.11.6", + "vitest": "^3.2.4" + }, + "version": "0.26.0" +} diff --git a/packages/common/s3-compat/src/index.ts b/packages/common/s3-compat/src/index.ts new file mode 100644 index 0000000000..1dc78d29a5 --- /dev/null +++ b/packages/common/s3-compat/src/index.ts @@ -0,0 +1,529 @@ +import { Buffer } from 'node:buffer'; +import { stringify as stringifyQuery } from 'node:querystring'; +import { Readable } from 'node:stream'; + +import aws4 from 'aws4'; +import { XMLParser } from 'fast-xml-parser'; +import { S3mini, sanitizeETag } from 's3mini'; + +export type S3CompatCredentials = { + accessKeyId: string; + secretAccessKey: string; + sessionToken?: string; +}; + +export type S3CompatConfig = { + endpoint: string; + region: string; + bucket: string; + forcePathStyle?: boolean; + requestTimeoutMs?: number; + minPartSize?: number; + presign?: { + expiresInSeconds: number; + signContentTypeForPut?: boolean; + }; +}; + +export type PresignedResult = { + url: string; + headers?: Record; + expiresAt: Date; +}; + +export type ListPartItem = { partNumber: number; etag: string }; + +export type ListObjectsItem = { + key: string; + lastModified: Date; + contentLength: number; +}; + +export interface S3CompatClient { + putObject( + key: string, + body: Blob | Buffer | Uint8Array | ReadableStream | Readable, + meta?: { contentType?: string; contentLength?: number } + ): Promise; + getObjectResponse(key: string): Promise; + headObject(key: string): Promise< + | { + contentType?: string; + contentLength?: number; + lastModified?: Date; + checksumCRC32?: string; + } + | undefined + >; + deleteObject(key: string): Promise; + listObjectsV2(prefix?: string): Promise; + + createMultipartUpload( + key: string, + meta?: { contentType?: string } + ): Promise<{ uploadId: string }>; + uploadPart( + key: string, + uploadId: string, + partNumber: number, + body: Blob | Buffer | Uint8Array | ReadableStream | Readable, + meta?: { contentLength?: number } + ): Promise<{ etag: string }>; + listParts(key: string, uploadId: string): Promise; + completeMultipartUpload( + key: string, + uploadId: string, + parts: ListPartItem[] + ): Promise; + abortMultipartUpload(key: string, uploadId: string): Promise; + + presignGetObject(key: string): Promise; + presignPutObject( + key: string, + meta?: { contentType?: string } + ): Promise; + presignUploadPart( + key: string, + uploadId: string, + partNumber: number + ): Promise; +} + +export type ParsedListParts = { + parts: ListPartItem[]; + isTruncated: boolean; + nextPartNumberMarker?: string; +}; + +const listPartsParser = new XMLParser({ + ignoreAttributes: false, + parseTagValue: false, + trimValues: true, +}); + +function asArray(value: T | T[] | undefined): T[] { + if (!value) return []; + return Array.isArray(value) ? value : [value]; +} + +function toBoolean(value: unknown): boolean { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') return value.toLowerCase() === 'true'; + return false; +} + +function joinPath(basePath: string, suffix: string) { + const trimmedBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + const trimmedSuffix = suffix.startsWith('/') ? suffix.slice(1) : suffix; + if (!trimmedBase) { + return `/${trimmedSuffix}`; + } + if (!trimmedSuffix) { + return trimmedBase; + } + return `${trimmedBase}/${trimmedSuffix}`; +} + +function encodeKey(key: string) { + return key.split('/').map(encodeURIComponent).join('/'); +} + +function buildQuery(params: Record) { + const entries = Object.entries(params).filter( + ([, value]) => value !== undefined + ); + if (entries.length === 0) return ''; + return stringifyQuery( + Object.fromEntries(entries.map(([key, value]) => [key, String(value)])) + ); +} + +function detectErrorCode(xml: string): string | undefined { + const parsed = listPartsParser.parse(xml); + if (!parsed || typeof parsed !== 'object') return undefined; + const error = (parsed as any).Error; + if (!error || typeof error !== 'object') return undefined; + const code = error.Code; + return typeof code === 'string' ? code : undefined; +} + +export function parseListPartsXml(xml: string): ParsedListParts { + const parsed = listPartsParser.parse(xml); + const root = + parsed?.ListPartsResult ?? + parsed?.ListPartsResult?.ListPartsResult ?? + parsed?.ListPartsResult; + const result = root && typeof root === 'object' ? root : parsed; + const partsNode = result?.Part; + + const parts = asArray(partsNode) + .map((part: any) => { + const partNumber = Number(part?.PartNumber); + const etag = + typeof part?.ETag === 'string' ? sanitizeETag(part.ETag) : ''; + if (!partNumber || !etag) return undefined; + return { partNumber, etag } satisfies ListPartItem; + }) + .filter((part): part is ListPartItem => !!part); + + const isTruncated = toBoolean(result?.IsTruncated); + const nextPartNumberMarker = + typeof result?.NextPartNumberMarker === 'string' + ? result?.NextPartNumberMarker + : result?.NextPartNumberMarker !== undefined + ? String(result?.NextPartNumberMarker) + : undefined; + + return { parts, isTruncated, nextPartNumberMarker }; +} + +function buildEndpoint(config: S3CompatConfig) { + const url = new URL(config.endpoint); + if (config.forcePathStyle) { + const segments = url.pathname.split('/').filter(Boolean); + if (segments[0] !== config.bucket) { + url.pathname = joinPath(url.pathname, config.bucket); + } + return url; + } + + const pathSegments = url.pathname.split('/').filter(Boolean); + const hostHasBucket = url.hostname.startsWith(`${config.bucket}.`); + const pathHasBucket = pathSegments[0] === config.bucket; + if (!hostHasBucket && !pathHasBucket) { + url.hostname = `${config.bucket}.${url.hostname}`; + } + return url; +} + +function shouldUseDuplex(init: RequestInit | undefined) { + if (!init?.body) return false; + if (typeof init.body === 'string') return false; + if (init.body instanceof ArrayBuffer) return false; + if (init.body instanceof Uint8Array) return false; + if (typeof Blob !== 'undefined' && init.body instanceof Blob) return false; + return true; +} + +export class S3Compat implements S3CompatClient { + private readonly client: S3mini; + private readonly endpoint: URL; + private readonly basePath: string; + private readonly region: string; + private readonly credentials: S3CompatCredentials; + private readonly presignConfig: { + expiresInSeconds: number; + signContentTypeForPut: boolean; + }; + private readonly fetchImpl: typeof fetch; + + constructor(config: S3CompatConfig, credentials: S3CompatCredentials) { + this.endpoint = buildEndpoint(config); + this.basePath = + this.endpoint.pathname === '/' ? '' : this.endpoint.pathname; + this.region = config.region; + this.credentials = credentials; + this.presignConfig = { + expiresInSeconds: config.presign?.expiresInSeconds ?? 60 * 60, + signContentTypeForPut: config.presign?.signContentTypeForPut ?? true, + }; + + const fetchImpl = globalThis.fetch.bind(globalThis); + this.fetchImpl = (input, init) => { + if (shouldUseDuplex(init)) { + return fetchImpl(input, { ...init, duplex: 'half' } as RequestInit); + } + return fetchImpl(input, init); + }; + + this.client = new S3mini({ + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + endpoint: this.endpoint.toString(), + region: config.region, + requestAbortTimeout: config.requestTimeoutMs, + minPartSize: config.minPartSize, + fetch: this.fetchImpl, + }); + } + + static fromConfig(config: S3CompatConfig, credentials: S3CompatCredentials) { + return new S3Compat(config, credentials); + } + + private buildObjectPath(key: string) { + const encodedKey = encodeKey(key); + return joinPath(this.basePath, encodedKey); + } + + private async signedFetch( + method: string, + key: string, + query?: Record, + headers?: Record + ) { + const path = this.buildObjectPath(key); + const queryString = query ? buildQuery(query) : ''; + const requestPath = queryString ? `${path}?${queryString}` : path; + const signed = aws4.sign( + { + method, + service: 's3', + region: this.region, + host: this.endpoint.host, + path: requestPath, + headers: headers ?? {}, + }, + this.credentials + ); + + const signedHeaders = Object.fromEntries( + Object.entries(signed.headers ?? {}).map(([key, value]) => [ + key, + String(value), + ]) + ); + + const url = `${this.endpoint.origin}${signed.path}`; + return this.fetchImpl(url, { method, headers: signedHeaders }); + } + + private presign( + method: string, + key: string, + query?: Record, + headers?: Record + ): PresignedResult { + const expiresInSeconds = this.presignConfig.expiresInSeconds; + const path = this.buildObjectPath(key); + const queryString = buildQuery({ + ...(query ?? {}), + 'X-Amz-Expires': expiresInSeconds, + }); + const requestPath = queryString ? `${path}?${queryString}` : path; + const signed = aws4.sign( + { + method, + service: 's3', + region: this.region, + host: this.endpoint.host, + path: requestPath, + headers: headers ?? {}, + signQuery: true, + }, + this.credentials + ); + + return { + url: `${this.endpoint.origin}${signed.path}`, + headers, + expiresAt: new Date(Date.now() + expiresInSeconds * 1000), + }; + } + + async putObject( + key: string, + body: Blob | Buffer | Uint8Array | ReadableStream | Readable, + meta?: { contentType?: string; contentLength?: number } + ): Promise { + const res = await this.client.putObject( + key, + body as any, + meta?.contentType, + undefined, + undefined, + meta?.contentLength + ); + if (!res.ok) { + throw new Error(`Failed to put object: ${res.status}`); + } + } + + async getObjectResponse(key: string) { + return this.client.getObjectResponse(key); + } + + async headObject(key: string) { + const res = await this.signedFetch('HEAD', key); + if (res.status === 404) { + return undefined; + } + + if (!res.ok) { + const errorBody = await res.text(); + const errorCode = detectErrorCode(errorBody); + if (errorCode === 'NoSuchKey' || errorCode === 'NotFound') { + return undefined; + } + throw new Error(`Failed to head object: ${res.status}`); + } + + const contentLengthHeader = res.headers.get('content-length'); + const contentLength = contentLengthHeader + ? Number(contentLengthHeader) + : undefined; + const contentType = res.headers.get('content-type') ?? undefined; + const lastModifiedHeader = res.headers.get('last-modified'); + const lastModified = lastModifiedHeader + ? new Date(lastModifiedHeader) + : undefined; + const checksumCRC32 = res.headers.get('x-amz-checksum-crc32') ?? undefined; + + return { + contentType, + contentLength, + lastModified, + checksumCRC32, + }; + } + + async deleteObject(key: string): Promise { + await this.client.deleteObject(key); + } + + async listObjectsV2(prefix?: string): Promise { + const results: ListObjectsItem[] = []; + let continuationToken: string | undefined; + do { + const page = await this.client.listObjectsPaged( + '/', + prefix ?? '', + 1000, + continuationToken + ); + if (!page || !page.objects) { + break; + } + for (const item of page.objects) { + results.push({ + key: item.Key, + lastModified: item.LastModified, + contentLength: item.Size, + }); + } + continuationToken = page.nextContinuationToken; + } while (continuationToken); + + return results; + } + + async createMultipartUpload( + key: string, + meta?: { contentType?: string } + ): Promise<{ uploadId: string }> { + const uploadId = await this.client.getMultipartUploadId( + key, + meta?.contentType + ); + return { uploadId }; + } + + async uploadPart( + key: string, + uploadId: string, + partNumber: number, + body: Blob | Buffer | Uint8Array | ReadableStream | Readable, + meta?: { contentLength?: number } + ): Promise<{ etag: string }> { + const additionalHeaders = meta?.contentLength + ? { 'Content-Length': String(meta.contentLength) } + : undefined; + const part = await this.client.uploadPart( + key, + uploadId, + body as any, + partNumber, + {}, + undefined, + additionalHeaders + ); + return { etag: part.etag }; + } + + async listParts( + key: string, + uploadId: string + ): Promise { + const parts: ListPartItem[] = []; + let partNumberMarker: string | undefined; + + while (true) { + const res = await this.signedFetch('GET', key, { + uploadId, + 'part-number-marker': partNumberMarker, + }); + + if (res.status === 404) { + return undefined; + } + + const body = await res.text(); + if (!res.ok) { + const errorCode = detectErrorCode(body); + if (errorCode === 'NoSuchUpload' || errorCode === 'NotFound') { + return undefined; + } + throw new Error(`Failed to list multipart upload parts: ${res.status}`); + } + + const parsed = parseListPartsXml(body); + parts.push(...parsed.parts); + + if (!parsed.isTruncated || !parsed.nextPartNumberMarker) { + break; + } + + partNumberMarker = parsed.nextPartNumberMarker; + } + + return parts; + } + + async completeMultipartUpload( + key: string, + uploadId: string, + parts: ListPartItem[] + ): Promise { + await this.client.completeMultipartUpload(key, uploadId, parts); + } + + async abortMultipartUpload(key: string, uploadId: string): Promise { + await this.client.abortMultipartUpload(key, uploadId); + } + + async presignGetObject(key: string): Promise { + return this.presign('GET', key); + } + + async presignPutObject( + key: string, + meta?: { contentType?: string } + ): Promise { + const contentType = meta?.contentType ?? 'application/octet-stream'; + const signContentType = this.presignConfig.signContentTypeForPut ?? true; + const headers = signContentType + ? { 'Content-Type': contentType } + : undefined; + const result = this.presign('PUT', key, undefined, headers); + + return { + ...result, + headers: headers ? { 'Content-Type': contentType } : undefined, + }; + } + + async presignUploadPart( + key: string, + uploadId: string, + partNumber: number + ): Promise { + return this.presign('PUT', key, { uploadId, partNumber }); + } +} + +export function createS3CompatClient( + config: S3CompatConfig, + credentials: S3CompatCredentials +) { + return new S3Compat(config, credentials); +} diff --git a/packages/common/s3-compat/tsconfig.json b/packages/common/s3-compat/tsconfig.json new file mode 100644 index 0000000000..e64f5e6230 --- /dev/null +++ b/packages/common/s3-compat/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.node.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "rootDir": "./src", + "outDir": "./dist", + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" + }, + "include": ["./src"], + "references": [] +} diff --git a/packages/frontend/admin/src/modules/settings/config.ts b/packages/frontend/admin/src/modules/settings/config.ts index 1a528daa8c..cfbe1183de 100644 --- a/packages/frontend/admin/src/modules/settings/config.ts +++ b/packages/frontend/admin/src/modules/settings/config.ts @@ -112,7 +112,7 @@ export const KNOWN_CONFIG_GROUPS = [ key: 'blob.storage', sub: 'config', type: 'JSON', - desc: 'The config passed directly to the storage provider(e.g. aws-sdk)', + desc: 'The S3 compatible config for the storage provider (endpoint/region/credentials).', }, { key: 'avatar.storage', @@ -131,7 +131,7 @@ export const KNOWN_CONFIG_GROUPS = [ key: 'avatar.storage', sub: 'config', type: 'JSON', - desc: 'The config passed directly to the storage provider(e.g. aws-sdk)', + desc: 'The S3 compatible config for the storage provider (endpoint/region/credentials).', }, { key: 'avatar.publicPath', @@ -175,7 +175,7 @@ export const KNOWN_CONFIG_GROUPS = [ key: 'storage', sub: 'config', type: 'JSON', - desc: 'The config passed directly to the storage provider(e.g. aws-sdk)', + desc: 'The S3 compatible config for the storage provider (endpoint/region/credentials).', }, ], } as ConfigGroup<'copilot'>, diff --git a/packages/frontend/core/src/modules/workspace-engine/utils/__tests__/base64.spec.ts b/packages/frontend/core/src/modules/workspace-engine/utils/__tests__/base64.spec.ts new file mode 100644 index 0000000000..ad13151d97 --- /dev/null +++ b/packages/frontend/core/src/modules/workspace-engine/utils/__tests__/base64.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'vitest'; + +import { base64ToUint8Array, uint8ArrayToBase64 } from '../base64'; + +function makeSample(size: number) { + const data = new Uint8Array(size); + for (let i = 0; i < size; i++) { + data[i] = (i * 13) % 251; + } + return data; +} + +describe('base64 helpers', () => { + test('roundtrip preserves data', async () => { + const input = makeSample(2048); + const encoded = await uint8ArrayToBase64(input); + const decoded = base64ToUint8Array(encoded); + expect(decoded).toEqual(input); + }); +}); diff --git a/packages/frontend/core/src/modules/workspace-engine/utils/base64.ts b/packages/frontend/core/src/modules/workspace-engine/utils/base64.ts index 7e2ffd3375..ab0c59cfdb 100644 --- a/packages/frontend/core/src/modules/workspace-engine/utils/base64.ts +++ b/packages/frontend/core/src/modules/workspace-engine/utils/base64.ts @@ -1,28 +1,37 @@ -export function uint8ArrayToBase64(array: Uint8Array): Promise { - return new Promise(resolve => { - // Create a blob from the Uint8Array - const blob = new Blob([array]); +type BufferConstructorLike = { + from( + data: Uint8Array | string, + encoding?: string + ): Uint8Array & { + toString(encoding: string): string; + }; +}; - const reader = new FileReader(); - reader.onload = function () { - const dataUrl = reader.result as string | null; - if (!dataUrl) { - resolve(''); - return; - } - // The result includes the `data:` URL prefix and the MIME type. We only want the Base64 data - const base64 = dataUrl.split(',')[1]; - resolve(base64); - }; +const BufferCtor = (globalThis as { Buffer?: BufferConstructorLike }).Buffer; +const CHUNK_SIZE = 0x8000; - reader.readAsDataURL(blob); - }); +export async function uint8ArrayToBase64(array: Uint8Array): Promise { + if (BufferCtor) { + return BufferCtor.from(array).toString('base64'); + } + + let binary = ''; + for (let i = 0; i < array.length; i += CHUNK_SIZE) { + const chunk = array.subarray(i, i + CHUNK_SIZE); + binary += String.fromCharCode(...chunk); + } + return btoa(binary); } export function base64ToUint8Array(base64: string) { + if (BufferCtor) { + return new Uint8Array(BufferCtor.from(base64, 'base64')); + } + const binaryString = atob(base64); - const binaryArray = [...binaryString].map(function (char) { - return char.charCodeAt(0); - }); - return new Uint8Array(binaryArray); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; } diff --git a/packages/frontend/mobile-native/Cargo.toml b/packages/frontend/mobile-native/Cargo.toml index bbd12cdffe..ef99a2d354 100644 --- a/packages/frontend/mobile-native/Cargo.toml +++ b/packages/frontend/mobile-native/Cargo.toml @@ -12,6 +12,7 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [features] +default = ["use-as-lib"] use-as-lib = ["affine_nbstore/use-as-lib"] [dependencies] diff --git a/packages/frontend/mobile-native/src/lib.rs b/packages/frontend/mobile-native/src/lib.rs index 9b4e70be54..84212c2670 100644 --- a/packages/frontend/mobile-native/src/lib.rs +++ b/packages/frontend/mobile-native/src/lib.rs @@ -89,11 +89,57 @@ impl TryFrom for affine_nbstore::DocUpdate { timestamp: chrono::DateTime::::from_timestamp_millis(update.timestamp) .ok_or(UniffiError::TimestampDecodingError)? .naive_utc(), - bin: update.bin.into(), + bin: Into::::into( + base64_simd::STANDARD + .decode_to_vec(update.bin) + .map_err(|e| UniffiError::Base64DecodingError(e.to_string()))?, + ), }) } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn doc_update_roundtrip_base64() { + let timestamp = chrono::DateTime::::from_timestamp_millis(1_700_000_000_000) + .unwrap() + .naive_utc(); + let original = affine_nbstore::DocUpdate { + doc_id: "doc-1".to_string(), + timestamp, + bin: vec![1, 2, 3, 4, 5], + }; + + let encoded: DocUpdate = original.into(); + let decoded = affine_nbstore::DocUpdate::try_from(encoded).unwrap(); + + assert_eq!(decoded.doc_id, "doc-1"); + assert_eq!(decoded.timestamp, timestamp); + assert_eq!(decoded.bin, vec![1, 2, 3, 4, 5]); + } + + #[test] + fn doc_update_rejects_invalid_base64() { + let update = DocUpdate { + doc_id: "doc-2".to_string(), + timestamp: 0, + bin: "not-base64!!".to_string(), + }; + + let err = match affine_nbstore::DocUpdate::try_from(update) { + Ok(_) => panic!("expected base64 decode error"), + Err(err) => err, + }; + match err { + UniffiError::Base64DecodingError(_) => {} + other => panic!("unexpected error: {other:?}"), + } + } +} + #[derive(uniffi::Record)] pub struct DocClock { pub doc_id: String, diff --git a/packages/frontend/native/Cargo.toml b/packages/frontend/native/Cargo.toml index c7bb670896..6656a06ec2 100644 --- a/packages/frontend/native/Cargo.toml +++ b/packages/frontend/native/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] affine_common = { workspace = true, features = ["hashcash"] } affine_media_capture = { path = "./media_capture" } -affine_nbstore = { path = "./nbstore" } +affine_nbstore = { workspace = true, features = ["napi"] } affine_sqlite_v1 = { path = "./sqlite_v1" } napi = { workspace = true } napi-derive = { workspace = true } @@ -25,6 +25,12 @@ sqlx = { workspace = true, default-features = false, features = [ thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } +[target.'cfg(not(target_os = "linux"))'.dependencies] +mimalloc = { workspace = true } + +[target.'cfg(all(target_os = "linux", not(target_arch = "arm")))'.dependencies] +mimalloc = { workspace = true, features = ["local_dynamic_tls"] } + [dev-dependencies] chrono = { workspace = true } serde_json = { workspace = true } diff --git a/packages/frontend/native/index.d.ts b/packages/frontend/native/index.d.ts index 99ebd4f4c5..e85784fa3b 100644 --- a/packages/frontend/native/index.d.ts +++ b/packages/frontend/native/index.d.ts @@ -19,10 +19,10 @@ export declare class ApplicationStateChangedSubscriber { } export declare class AudioCaptureSession { + stop(): void get sampleRate(): number get channels(): number get actualSampleRate(): number - stop(): void } export declare class ShareableContent { @@ -31,9 +31,9 @@ export declare class ShareableContent { constructor() static applications(): Array static applicationWithProcessId(processId: number): ApplicationInfo | null + static isUsingMicrophone(processId: number): boolean static tapAudio(processId: number, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioCaptureSession static tapGlobalAudio(excludedProcesses: Array | undefined | null, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioCaptureSession - static isUsingMicrophone(processId: number): boolean } export declare function decodeAudio(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null, signal?: AbortSignal | undefined | null): Promise diff --git a/packages/frontend/native/nbstore/Cargo.toml b/packages/frontend/native/nbstore/Cargo.toml index 5ef99e5a94..f5ac3550c7 100644 --- a/packages/frontend/native/nbstore/Cargo.toml +++ b/packages/frontend/native/nbstore/Cargo.toml @@ -7,6 +7,8 @@ version = "0.0.0" crate-type = ["cdylib", "rlib"] [features] +default = [] +napi = ["affine_common/napi"] use-as-lib = ["napi-derive/noop", "napi/noop"] [dependencies] diff --git a/packages/frontend/native/nbstore/src/lib.rs b/packages/frontend/native/nbstore/src/lib.rs index 89fc1f6f60..80aea4b7dd 100644 --- a/packages/frontend/native/nbstore/src/lib.rs +++ b/packages/frontend/native/nbstore/src/lib.rs @@ -8,6 +8,8 @@ pub mod indexer_sync; pub mod pool; pub mod storage; +#[cfg(not(feature = "use-as-lib"))] +use affine_common::napi_utils::to_napi_error; use chrono::NaiveDateTime; use napi::bindgen_prelude::*; use napi_derive::napi; @@ -23,7 +25,7 @@ type Result = napi::Result; #[cfg(not(feature = "use-as-lib"))] impl From for napi::Error { fn from(err: error::Error) -> Self { - napi::Error::new(napi::Status::GenericFailure, err.to_string()) + to_napi_error(err, napi::Status::GenericFailure) } } @@ -491,3 +493,15 @@ impl DocStorage { Ok(()) } } + +#[cfg(all(test, not(feature = "use-as-lib")))] +mod tests { + use super::error; + + #[test] + fn napi_error_mapping_preserves_reason() { + let err: napi::Error = error::Error::InvalidOperation.into(); + assert_eq!(err.status, napi::Status::GenericFailure); + assert!(err.reason.contains("Invalid operation")); + } +} diff --git a/packages/frontend/native/src/hashcash.rs b/packages/frontend/native/src/hashcash.rs index bda991081f..27262f2dcf 100644 --- a/packages/frontend/native/src/hashcash.rs +++ b/packages/frontend/native/src/hashcash.rs @@ -64,3 +64,27 @@ impl Task for AsyncMintChallengeResponse { pub fn mint_challenge_response(resource: String, bits: Option) -> AsyncTask { AsyncTask::new(AsyncMintChallengeResponse { bits, resource }) } + +#[cfg(test)] +mod tests { + use napi::Task; + + use super::*; + + #[test] + fn hashcash_roundtrip() { + let resource = "test-resource".to_string(); + let mut mint = AsyncMintChallengeResponse { + bits: Some(8), + resource: resource.clone(), + }; + let stamp = mint.compute().unwrap(); + + let mut verify = AsyncVerifyChallengeResponse { + response: stamp, + bits: 8, + resource, + }; + assert!(verify.compute().unwrap()); + } +} diff --git a/packages/frontend/native/src/lib.rs b/packages/frontend/native/src/lib.rs index b17678a551..63582db8fb 100644 --- a/packages/frontend/native/src/lib.rs +++ b/packages/frontend/native/src/lib.rs @@ -1,5 +1,9 @@ pub mod hashcash; +#[cfg(not(target_arch = "arm"))] +#[global_allocator] +static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; + #[allow(unused_imports)] pub use affine_media_capture::*; pub use affine_nbstore::*; diff --git a/tests/blocksuite/e2e/edgeless/clipboard.spec.ts b/tests/blocksuite/e2e/edgeless/clipboard.spec.ts index 52a6a7f3db..f87ece35c8 100644 --- a/tests/blocksuite/e2e/edgeless/clipboard.spec.ts +++ b/tests/blocksuite/e2e/edgeless/clipboard.spec.ts @@ -173,6 +173,21 @@ test.describe('frame clipboard', () => { }); test.describe('pasting URLs', () => { + test.beforeEach(async ({ page }) => { + await page.route( + 'https://affine-worker.toeverything.workers.dev/api/worker/link-preview', + async route => { + await route.fulfill({ + json: {}, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }, + }); + } + ); + }); + test('pasting github pr url', async ({ page }) => { await commonSetup(page); await waitNextFrame(page); diff --git a/tools/cli/package.json b/tools/cli/package.json index 600081a041..f03a666d2a 100644 --- a/tools/cli/package.json +++ b/tools/cli/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@affine-tools/utils": "workspace:*", - "@aws-sdk/client-s3": "^3.948.0", + "@affine/s3-compat": "workspace:*", "@napi-rs/simple-git": "^0.1.22", "@perfsee/webpack": "^1.13.0", "@sentry/webpack-plugin": "^3.0.0", diff --git a/tools/cli/src/webpack/s3-plugin.ts b/tools/cli/src/webpack/s3-plugin.ts index 00de2355c6..4e5459d7ac 100644 --- a/tools/cli/src/webpack/s3-plugin.ts +++ b/tools/cli/src/webpack/s3-plugin.ts @@ -1,8 +1,7 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import type { PutObjectCommandInput } from '@aws-sdk/client-s3'; -import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { createS3CompatClient } from '@affine/s3-compat'; import { lookup } from 'mime-types'; import type { Compiler, WebpackPluginInstance } from 'webpack'; @@ -11,16 +10,18 @@ export const R2_BUCKET = (process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod'); export class WebpackS3Plugin implements WebpackPluginInstance { - private readonly s3 = new S3Client({ - region: 'auto', - endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, - credentials: { + private readonly s3 = createS3CompatClient( + { + region: 'auto', + bucket: R2_BUCKET, + forcePathStyle: true, + endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, + }, + { accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, - }, - requestChecksumCalculation: 'WHEN_REQUIRED', - responseChecksumValidation: 'WHEN_REQUIRED', - }); + } + ); apply(compiler: Compiler) { compiler.hooks.assetEmitted.tapPromise( @@ -31,16 +32,11 @@ export class WebpackS3Plugin implements WebpackPluginInstance { } const assetPath = join(outputPath, asset); const assetSource = await readFile(assetPath); - const putObjectCommandOptions: PutObjectCommandInput = { - Body: assetSource, - Bucket: R2_BUCKET, - Key: asset, - }; - const contentType = lookup(asset); - if (contentType) { - putObjectCommandOptions.ContentType = contentType; - } - await this.s3.send(new PutObjectCommand(putObjectCommandOptions)); + const contentType = lookup(asset) || undefined; + await this.s3.putObject(asset, assetSource, { + contentType, + contentLength: assetSource.byteLength, + }); } ); } diff --git a/tools/cli/tsconfig.json b/tools/cli/tsconfig.json index ab3bade10a..006bdb5791 100644 --- a/tools/cli/tsconfig.json +++ b/tools/cli/tsconfig.json @@ -6,5 +6,8 @@ "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" }, "include": ["./src"], - "references": [{ "path": "../utils" }] + "references": [ + { "path": "../utils" }, + { "path": "../../packages/common/s3-compat" } + ] } diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 3ecd85844e..4168f3b7cc 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -1166,6 +1166,7 @@ export const PackageList = [ location: 'packages/backend/server', name: '@affine/server', workspaceDependencies: [ + 'packages/common/s3-compat', 'packages/backend/native', 'tools/cli', 'tools/utils', @@ -1222,6 +1223,11 @@ export const PackageList = [ name: '@affine/reader', workspaceDependencies: ['blocksuite/affine/all'], }, + { + location: 'packages/common/s3-compat', + name: '@affine/s3-compat', + workspaceDependencies: [], + }, { location: 'packages/frontend/admin', name: '@affine/admin', @@ -1462,7 +1468,7 @@ export const PackageList = [ { location: 'tools/cli', name: '@affine-tools/cli', - workspaceDependencies: ['tools/utils'], + workspaceDependencies: ['tools/utils', 'packages/common/s3-compat'], }, { location: 'tools/commitlint', @@ -1580,6 +1586,7 @@ export type PackageName = | '@toeverything/infra' | '@affine/nbstore' | '@affine/reader' + | '@affine/s3-compat' | '@affine/admin' | '@affine/android' | '@affine/electron' diff --git a/tsconfig.json b/tsconfig.json index 37dec242cb..fe167fcb0d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -132,6 +132,7 @@ { "path": "./packages/common/infra" }, { "path": "./packages/common/nbstore" }, { "path": "./packages/common/reader" }, + { "path": "./packages/common/s3-compat" }, { "path": "./packages/frontend/admin" }, { "path": "./packages/frontend/apps/android" }, { "path": "./packages/frontend/apps/electron" }, diff --git a/yarn.lock b/yarn.lock index 7766bfd3c0..74ddf61839 100644 --- a/yarn.lock +++ b/yarn.lock @@ -116,7 +116,7 @@ __metadata: resolution: "@affine-tools/cli@workspace:tools/cli" dependencies: "@affine-tools/utils": "workspace:*" - "@aws-sdk/client-s3": "npm:^3.948.0" + "@affine/s3-compat": "workspace:*" "@napi-rs/simple-git": "npm:^0.1.22" "@perfsee/webpack": "npm:^1.13.0" "@sentry/webpack-plugin": "npm:^3.0.0" @@ -926,6 +926,18 @@ __metadata: languageName: unknown linkType: soft +"@affine/s3-compat@workspace:*, @affine/s3-compat@workspace:packages/common/s3-compat": + version: 0.0.0-use.local + resolution: "@affine/s3-compat@workspace:packages/common/s3-compat" + dependencies: + "@types/aws4": "npm:^1.11.6" + aws4: "npm:^1.13.2" + fast-xml-parser: "npm:^5.3.4" + s3mini: "npm:^0.9.1" + vitest: "npm:^3.2.4" + languageName: unknown + linkType: soft + "@affine/server-native@workspace:*, @affine/server-native@workspace:packages/backend/native": version: 0.0.0-use.local resolution: "@affine/server-native@workspace:packages/backend/native" @@ -942,6 +954,7 @@ __metadata: "@affine-tools/cli": "workspace:*" "@affine-tools/utils": "workspace:*" "@affine/graphql": "workspace:*" + "@affine/s3-compat": "workspace:*" "@affine/server-native": "workspace:*" "@ai-sdk/anthropic": "npm:^2.0.54" "@ai-sdk/google": "npm:^2.0.45" @@ -950,8 +963,6 @@ __metadata: "@ai-sdk/openai-compatible": "npm:^1.0.28" "@ai-sdk/perplexity": "npm:^2.0.21" "@apollo/server": "npm:^4.12.2" - "@aws-sdk/client-s3": "npm:^3.948.0" - "@aws-sdk/s3-request-presigner": "npm:^3.948.0" "@faker-js/faker": "npm:^10.1.0" "@fal-ai/serverless-client": "npm:^0.15.0" "@google-cloud/opentelemetry-cloud-trace-exporter": "npm:^3.0.0" @@ -1551,711 +1562,6 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/crc32@npm:5.2.0": - version: 5.2.0 - resolution: "@aws-crypto/crc32@npm:5.2.0" - dependencies: - "@aws-crypto/util": "npm:^5.2.0" - "@aws-sdk/types": "npm:^3.222.0" - tslib: "npm:^2.6.2" - checksum: 10/1b0a56ad4cb44c9512d8b1668dcf9306ab541d3a73829f435ca97abaec8d56f3db953db03ad0d0698754fea16fcd803d11fa42e0889bc7b803c6a030b04c63de - languageName: node - linkType: hard - -"@aws-crypto/crc32c@npm:5.2.0": - version: 5.2.0 - resolution: "@aws-crypto/crc32c@npm:5.2.0" - dependencies: - "@aws-crypto/util": "npm:^5.2.0" - "@aws-sdk/types": "npm:^3.222.0" - tslib: "npm:^2.6.2" - checksum: 10/08bd1db17d7c772fa6e34b38a360ce77ad041164743113eefa8343c2af917a419697daf090c5854129ef19f3a9673ed1fd8446e03eb32c8ed52d2cc409b0dee7 - languageName: node - linkType: hard - -"@aws-crypto/sha1-browser@npm:5.2.0": - version: 5.2.0 - resolution: "@aws-crypto/sha1-browser@npm:5.2.0" - dependencies: - "@aws-crypto/supports-web-crypto": "npm:^5.2.0" - "@aws-crypto/util": "npm:^5.2.0" - "@aws-sdk/types": "npm:^3.222.0" - "@aws-sdk/util-locate-window": "npm:^3.0.0" - "@smithy/util-utf8": "npm:^2.0.0" - tslib: "npm:^2.6.2" - checksum: 10/239f4c59cce9abd33c01117b10553fbef868a063e74faf17edb798c250d759a2578841efa2837e5e51854f52ef57dbc40780b073cae20f89ebed6a8cc7fa06f1 - languageName: node - linkType: hard - -"@aws-crypto/sha256-browser@npm:5.2.0": - version: 5.2.0 - resolution: "@aws-crypto/sha256-browser@npm:5.2.0" - dependencies: - "@aws-crypto/sha256-js": "npm:^5.2.0" - "@aws-crypto/supports-web-crypto": "npm:^5.2.0" - "@aws-crypto/util": "npm:^5.2.0" - "@aws-sdk/types": "npm:^3.222.0" - "@aws-sdk/util-locate-window": "npm:^3.0.0" - "@smithy/util-utf8": "npm:^2.0.0" - tslib: "npm:^2.6.2" - checksum: 10/2b1b701ca6caa876333b4eb2b96e5187d71ebb51ebf8e2d632690dbcdedeff038202d23adcc97e023437ed42bb1963b7b463e343687edf0635fd4b98b2edad1a - languageName: node - linkType: hard - -"@aws-crypto/sha256-js@npm:5.2.0, @aws-crypto/sha256-js@npm:^5.2.0": - version: 5.2.0 - resolution: "@aws-crypto/sha256-js@npm:5.2.0" - dependencies: - "@aws-crypto/util": "npm:^5.2.0" - "@aws-sdk/types": "npm:^3.222.0" - tslib: "npm:^2.6.2" - checksum: 10/f46aace7b873c615be4e787ab0efd0148ef7de48f9f12c7d043e05c52e52b75bb0bf6dbcb9b2852d940d7724fab7b6d5ff1469160a3dd024efe7a68b5f70df8c - languageName: node - linkType: hard - -"@aws-crypto/supports-web-crypto@npm:^5.2.0": - version: 5.2.0 - resolution: "@aws-crypto/supports-web-crypto@npm:5.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/6ed0c7e17f4f6663d057630805c45edb35d5693380c24ab52d4c453ece303c6c8a6ade9ee93c97dda77d9f6cae376ffbb44467057161c513dffa3422250edaf5 - languageName: node - linkType: hard - -"@aws-crypto/util@npm:5.2.0, @aws-crypto/util@npm:^5.2.0": - version: 5.2.0 - resolution: "@aws-crypto/util@npm:5.2.0" - dependencies: - "@aws-sdk/types": "npm:^3.222.0" - "@smithy/util-utf8": "npm:^2.0.0" - tslib: "npm:^2.6.2" - checksum: 10/f80a174c404e1ad4364741c942f440e75f834c08278fa754349fe23a6edc679d480ea9ced5820774aee58091ed270067022d8059ecf1a7ef452d58134ac7e9e1 - languageName: node - linkType: hard - -"@aws-sdk/client-s3@npm:^3.948.0": - version: 3.980.0 - resolution: "@aws-sdk/client-s3@npm:3.980.0" - dependencies: - "@aws-crypto/sha1-browser": "npm:5.2.0" - "@aws-crypto/sha256-browser": "npm:5.2.0" - "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/credential-provider-node": "npm:^3.972.4" - "@aws-sdk/middleware-bucket-endpoint": "npm:^3.972.3" - "@aws-sdk/middleware-expect-continue": "npm:^3.972.3" - "@aws-sdk/middleware-flexible-checksums": "npm:^3.972.3" - "@aws-sdk/middleware-host-header": "npm:^3.972.3" - "@aws-sdk/middleware-location-constraint": "npm:^3.972.3" - "@aws-sdk/middleware-logger": "npm:^3.972.3" - "@aws-sdk/middleware-recursion-detection": "npm:^3.972.3" - "@aws-sdk/middleware-sdk-s3": "npm:^3.972.5" - "@aws-sdk/middleware-ssec": "npm:^3.972.3" - "@aws-sdk/middleware-user-agent": "npm:^3.972.5" - "@aws-sdk/region-config-resolver": "npm:^3.972.3" - "@aws-sdk/signature-v4-multi-region": "npm:3.980.0" - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/util-endpoints": "npm:3.980.0" - "@aws-sdk/util-user-agent-browser": "npm:^3.972.3" - "@aws-sdk/util-user-agent-node": "npm:^3.972.3" - "@smithy/config-resolver": "npm:^4.4.6" - "@smithy/core": "npm:^3.22.0" - "@smithy/eventstream-serde-browser": "npm:^4.2.8" - "@smithy/eventstream-serde-config-resolver": "npm:^4.3.8" - "@smithy/eventstream-serde-node": "npm:^4.2.8" - "@smithy/fetch-http-handler": "npm:^5.3.9" - "@smithy/hash-blob-browser": "npm:^4.2.9" - "@smithy/hash-node": "npm:^4.2.8" - "@smithy/hash-stream-node": "npm:^4.2.8" - "@smithy/invalid-dependency": "npm:^4.2.8" - "@smithy/md5-js": "npm:^4.2.8" - "@smithy/middleware-content-length": "npm:^4.2.8" - "@smithy/middleware-endpoint": "npm:^4.4.12" - "@smithy/middleware-retry": "npm:^4.4.29" - "@smithy/middleware-serde": "npm:^4.2.9" - "@smithy/middleware-stack": "npm:^4.2.8" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/node-http-handler": "npm:^4.4.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/url-parser": "npm:^4.2.8" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-body-length-node": "npm:^4.2.1" - "@smithy/util-defaults-mode-browser": "npm:^4.3.28" - "@smithy/util-defaults-mode-node": "npm:^4.2.31" - "@smithy/util-endpoints": "npm:^3.2.8" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-retry": "npm:^4.2.8" - "@smithy/util-stream": "npm:^4.5.10" - "@smithy/util-utf8": "npm:^4.2.0" - "@smithy/util-waiter": "npm:^4.2.8" - tslib: "npm:^2.6.2" - checksum: 10/52867aeab4dee02556a10e84a2c74d67888daa039f3ca8417b28846a4886283f7465037cdfd9795bc5cbc4d71d18fd413a17a96227dcffc462aec31152f3193c - languageName: node - linkType: hard - -"@aws-sdk/client-sso@npm:3.980.0": - version: 3.980.0 - resolution: "@aws-sdk/client-sso@npm:3.980.0" - dependencies: - "@aws-crypto/sha256-browser": "npm:5.2.0" - "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/middleware-host-header": "npm:^3.972.3" - "@aws-sdk/middleware-logger": "npm:^3.972.3" - "@aws-sdk/middleware-recursion-detection": "npm:^3.972.3" - "@aws-sdk/middleware-user-agent": "npm:^3.972.5" - "@aws-sdk/region-config-resolver": "npm:^3.972.3" - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/util-endpoints": "npm:3.980.0" - "@aws-sdk/util-user-agent-browser": "npm:^3.972.3" - "@aws-sdk/util-user-agent-node": "npm:^3.972.3" - "@smithy/config-resolver": "npm:^4.4.6" - "@smithy/core": "npm:^3.22.0" - "@smithy/fetch-http-handler": "npm:^5.3.9" - "@smithy/hash-node": "npm:^4.2.8" - "@smithy/invalid-dependency": "npm:^4.2.8" - "@smithy/middleware-content-length": "npm:^4.2.8" - "@smithy/middleware-endpoint": "npm:^4.4.12" - "@smithy/middleware-retry": "npm:^4.4.29" - "@smithy/middleware-serde": "npm:^4.2.9" - "@smithy/middleware-stack": "npm:^4.2.8" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/node-http-handler": "npm:^4.4.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/url-parser": "npm:^4.2.8" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-body-length-node": "npm:^4.2.1" - "@smithy/util-defaults-mode-browser": "npm:^4.3.28" - "@smithy/util-defaults-mode-node": "npm:^4.2.31" - "@smithy/util-endpoints": "npm:^3.2.8" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-retry": "npm:^4.2.8" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/c2715478f72b9d022de424c767e7c57a56e043d03e6fd9930baa11f228da8cda7173561706385e3617d5723cb71e4e8999eb3ddc62430940cc89df48f1c62ce7 - languageName: node - linkType: hard - -"@aws-sdk/core@npm:^3.973.5": - version: 3.973.5 - resolution: "@aws-sdk/core@npm:3.973.5" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/xml-builder": "npm:^3.972.2" - "@smithy/core": "npm:^3.22.0" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/signature-v4": "npm:^5.3.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/f5021f131d9755d72f3e4d871bc75c1eeaf40b944a1f2e483a0166bf1b9b5b9622fce41f3568d3a6ec58da03c811cc7008399edb86bd5d32f10a963a20870101 - languageName: node - linkType: hard - -"@aws-sdk/crc64-nvme@npm:3.972.0": - version: 3.972.0 - resolution: "@aws-sdk/crc64-nvme@npm:3.972.0" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/47d41dfbff4ed7664d1cc4565f4b190cdf6d87c7b550897a709894ba041c6d4c28171cf7089365af8441bf40234167df916f56bd4ea7c7cd6ba31cab56ed28b1 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-env@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/credential-provider-env@npm:3.972.3" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/a67ccab5c46d7b336ebe91ca8bb93c1741115c067b9243ed6f2164c921001fe5a798e84786381d9d03bc4ff07b4aeb1b0094404a9bac0674a0e975419709f7e4 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-http@npm:^3.972.5": - version: 3.972.5 - resolution: "@aws-sdk/credential-provider-http@npm:3.972.5" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/fetch-http-handler": "npm:^5.3.9" - "@smithy/node-http-handler": "npm:^4.4.8" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-stream": "npm:^4.5.10" - tslib: "npm:^2.6.2" - checksum: 10/55fd400d28ac906049a87090923ee6cecfbd8c182dd32ee699f3109c3e1c165aa9819c042d9e73f07802675aee620de41c348cc4794588ff7d231c4ff54dddcf - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-ini@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/credential-provider-ini@npm:3.972.3" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/credential-provider-env": "npm:^3.972.3" - "@aws-sdk/credential-provider-http": "npm:^3.972.5" - "@aws-sdk/credential-provider-login": "npm:^3.972.3" - "@aws-sdk/credential-provider-process": "npm:^3.972.3" - "@aws-sdk/credential-provider-sso": "npm:^3.972.3" - "@aws-sdk/credential-provider-web-identity": "npm:^3.972.3" - "@aws-sdk/nested-clients": "npm:3.980.0" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/credential-provider-imds": "npm:^4.2.8" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/22ecb40caf4ef4217c403d32bc809837cc0a78431af6004ca25a7d82597835aa00af0e387b826a130570059f1eab1229ce9e0f0c555e39b1218ca229d98dc538 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-login@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/credential-provider-login@npm:3.972.3" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/nested-clients": "npm:3.980.0" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/4ac4bd7d38f691311d9bac46e2986943a67ae4fc3d8d15f4539b2ef6d22608e564d0fe007b46815d780ec2de8c37c86b322387789fd05593484e338163691bc7 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-node@npm:^3.972.4": - version: 3.972.4 - resolution: "@aws-sdk/credential-provider-node@npm:3.972.4" - dependencies: - "@aws-sdk/credential-provider-env": "npm:^3.972.3" - "@aws-sdk/credential-provider-http": "npm:^3.972.5" - "@aws-sdk/credential-provider-ini": "npm:^3.972.3" - "@aws-sdk/credential-provider-process": "npm:^3.972.3" - "@aws-sdk/credential-provider-sso": "npm:^3.972.3" - "@aws-sdk/credential-provider-web-identity": "npm:^3.972.3" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/credential-provider-imds": "npm:^4.2.8" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/0ee3ad056d78f67f9c8afe78ab46f82ceca7e432079ec1a1c3db29d23ec0c67959c72ece571d3a143d1eab78158825aac720d5c3f47715984ab0dff27c619400 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-process@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/credential-provider-process@npm:3.972.3" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/37421140a546a9a45e890d8d32e8214a5b1b0ed80844031d9deb5c3e2ab2cb5b52242ee9f72795310d194fc54ffc51f6f15f9d36ae07b1ccd32873f99b9fba41 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-sso@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/credential-provider-sso@npm:3.972.3" - dependencies: - "@aws-sdk/client-sso": "npm:3.980.0" - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/token-providers": "npm:3.980.0" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/f025a35c068548be28b5e1343e52102b09b9fd9e01d0bc433700d68cd06007b92ea56c836c79ec74612ad7fce1112a65293a81e85961aa09023a7b39049cf271 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-web-identity@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.3" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/nested-clients": "npm:3.980.0" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/0022c2e17f2bab8d39d0b3875a6ac65631e984256ea95fb5e0b67cab19a38e0fdc02ce3089051b8307d3ad7ddfef0211e94b76ee39e40fe90ca7e587c740dbe2 - languageName: node - linkType: hard - -"@aws-sdk/middleware-bucket-endpoint@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/util-arn-parser": "npm:^3.972.2" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-config-provider": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/5e0906a76ab6f200901759537fb69034546d228405b12b02b64e04f85aefacda0e0818f07d8595617b9956f135fc56545827624f9652858e27da231240cbb9b3 - languageName: node - linkType: hard - -"@aws-sdk/middleware-expect-continue@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/96c2d64294e9482873345543a2d1c11a67941bde5dfdb32c1c05b578a394083583e53c6a1c2c3ccee41e4937391ae38878b7c03fd2b5ba08e06567926e34a248 - languageName: node - linkType: hard - -"@aws-sdk/middleware-flexible-checksums@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.972.3" - dependencies: - "@aws-crypto/crc32": "npm:5.2.0" - "@aws-crypto/crc32c": "npm:5.2.0" - "@aws-crypto/util": "npm:5.2.0" - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/crc64-nvme": "npm:3.972.0" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/is-array-buffer": "npm:^4.2.0" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-stream": "npm:^4.5.10" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/0ba04273b21ffaee56d444dc2c6c65e0f75c2f823ad1ff78973fac959a1c57ad2429f0c6d19e1366830e8981fda471c79d8d07b1cf8389690f7d2f7b45dce340 - languageName: node - linkType: hard - -"@aws-sdk/middleware-host-header@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-host-header@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/14b6e32f32f1c8b0e66a396b092785d3d597b27df696ed2daf8310d2a463416bcc89480043b6a5083698403fc85904caf5ebbcb0fbd12f89f05dbf10878d2cc7 - languageName: node - linkType: hard - -"@aws-sdk/middleware-location-constraint@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/9c9677e07af9db00af5f748aae79321ec9fb3888b508704e1de0a1fbcf19e1f254037274324d17fc1c11f24ad60c075024560784f0e9958b4868da3e24e9460b - languageName: node - linkType: hard - -"@aws-sdk/middleware-logger@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-logger@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/abda3a05b73a2056fbe0d2aa139ee5ad590733d7ef96a18c2ca92b314795ba3fe83216668bd731b8a40f7951b1147eb1ed3566c1b33ee9b8ae9994089596e3b8 - languageName: node - linkType: hard - -"@aws-sdk/middleware-recursion-detection@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@aws/lambda-invoke-store": "npm:^0.2.2" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/8308e8eb1344669bca86613f160768dd39640ca3ed37730b579a6f71be14f6deed7acdb4f3d195a7f8c5a130afb82411dc18c8a361f7dc1f769c9dc240aaa16f - languageName: node - linkType: hard - -"@aws-sdk/middleware-sdk-s3@npm:^3.972.5": - version: 3.972.5 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.972.5" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/util-arn-parser": "npm:^3.972.2" - "@smithy/core": "npm:^3.22.0" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/signature-v4": "npm:^5.3.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-config-provider": "npm:^4.2.0" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-stream": "npm:^4.5.10" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/94aef879d027d2bd99facbf485ad6bd0219905f62825c4abda59c69813d9f68b0221dfd347015bcb7cdb848a764cdec1b84630ec86b59c0cad1125bd082e874b - languageName: node - linkType: hard - -"@aws-sdk/middleware-ssec@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/middleware-ssec@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/6510039afd2f1dce5b9b4870123fb269b6315246a58111d7b08849fff1dd4312f10f39ca69dc5838406c3b7063923fc182dd746cb6543934b41f6f4a29f61980 - languageName: node - linkType: hard - -"@aws-sdk/middleware-user-agent@npm:^3.972.5": - version: 3.972.5 - resolution: "@aws-sdk/middleware-user-agent@npm:3.972.5" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/util-endpoints": "npm:3.980.0" - "@smithy/core": "npm:^3.22.0" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/2e77b0b5c15eef3ce192c403c86f31ff20418d2657fda4d66f0bd7997116cf5638610e9b277fc2be9fb86ae63f3f804706e7cd96bec839602d350adf800c5f4c - languageName: node - linkType: hard - -"@aws-sdk/nested-clients@npm:3.980.0": - version: 3.980.0 - resolution: "@aws-sdk/nested-clients@npm:3.980.0" - dependencies: - "@aws-crypto/sha256-browser": "npm:5.2.0" - "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/middleware-host-header": "npm:^3.972.3" - "@aws-sdk/middleware-logger": "npm:^3.972.3" - "@aws-sdk/middleware-recursion-detection": "npm:^3.972.3" - "@aws-sdk/middleware-user-agent": "npm:^3.972.5" - "@aws-sdk/region-config-resolver": "npm:^3.972.3" - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/util-endpoints": "npm:3.980.0" - "@aws-sdk/util-user-agent-browser": "npm:^3.972.3" - "@aws-sdk/util-user-agent-node": "npm:^3.972.3" - "@smithy/config-resolver": "npm:^4.4.6" - "@smithy/core": "npm:^3.22.0" - "@smithy/fetch-http-handler": "npm:^5.3.9" - "@smithy/hash-node": "npm:^4.2.8" - "@smithy/invalid-dependency": "npm:^4.2.8" - "@smithy/middleware-content-length": "npm:^4.2.8" - "@smithy/middleware-endpoint": "npm:^4.4.12" - "@smithy/middleware-retry": "npm:^4.4.29" - "@smithy/middleware-serde": "npm:^4.2.9" - "@smithy/middleware-stack": "npm:^4.2.8" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/node-http-handler": "npm:^4.4.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/url-parser": "npm:^4.2.8" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-body-length-node": "npm:^4.2.1" - "@smithy/util-defaults-mode-browser": "npm:^4.3.28" - "@smithy/util-defaults-mode-node": "npm:^4.2.31" - "@smithy/util-endpoints": "npm:^3.2.8" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-retry": "npm:^4.2.8" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/601bcf7ec78ca3ffa476d069a17182364fcc6cc4812bf0550af2d4fa58be2b87eb0da1a0c6ba25d3c361aa2a7a05bed6c1e3a25fa8aba8c87037fff97a237b2e - languageName: node - linkType: hard - -"@aws-sdk/region-config-resolver@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/region-config-resolver@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/config-resolver": "npm:^4.4.6" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/8512a573492a990b028d9f0058d6034d54fb186af20d1da9529ac3d5f8d435c43fa16ef7d3dc0b3ffa679bb90529b55b0d00619160a3549839a136cc698fefb8 - languageName: node - linkType: hard - -"@aws-sdk/s3-request-presigner@npm:^3.948.0": - version: 3.980.0 - resolution: "@aws-sdk/s3-request-presigner@npm:3.980.0" - dependencies: - "@aws-sdk/signature-v4-multi-region": "npm:3.980.0" - "@aws-sdk/types": "npm:^3.973.1" - "@aws-sdk/util-format-url": "npm:^3.972.3" - "@smithy/middleware-endpoint": "npm:^4.4.12" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/b0b92bf7c8270647acbd5eadf1fd1c2ed2ac363d30e2075a8e410fb4f670f4400490c6f4dd3ae5a072837bf719ec6c8d199a4c652ca9464dcc02633f4082c80c - languageName: node - linkType: hard - -"@aws-sdk/signature-v4-multi-region@npm:3.980.0": - version: 3.980.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.980.0" - dependencies: - "@aws-sdk/middleware-sdk-s3": "npm:^3.972.5" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/signature-v4": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/456e1617c1f51289616e858181ee84f1d2424abd21dd27ff3a9b1c1834fa07cf88e34674321a20b1a15533988adeced6cb07888a7b61a3988e98044c2189b7f5 - languageName: node - linkType: hard - -"@aws-sdk/token-providers@npm:3.980.0": - version: 3.980.0 - resolution: "@aws-sdk/token-providers@npm:3.980.0" - dependencies: - "@aws-sdk/core": "npm:^3.973.5" - "@aws-sdk/nested-clients": "npm:3.980.0" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/b9d903cc9d84b95a9260001e617eb2bab3c037a7778bd22728e85a02ad48d207771b7d803135653919ed9d89ca4c0a9dc535cec8d728cde5a7e8dc5569482cbb - languageName: node - linkType: hard - -"@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.973.1": - version: 3.973.1 - resolution: "@aws-sdk/types@npm:3.973.1" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/9cdcb457d6110a88a547fe26922d43450bf7685b26034e935c72c1717de90a22541f298ce4e76fde564d3af11908928b1584b856085dcb175f9bb08853d1a575 - languageName: node - linkType: hard - -"@aws-sdk/util-arn-parser@npm:^3.972.2": - version: 3.972.2 - resolution: "@aws-sdk/util-arn-parser@npm:3.972.2" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/6c09725259187615199b44c21cc9aaf6e61c4d1f326535fd36cf1e95d9842bd58084542c72a9facbca47c5846c5bd8fed7b179e86a036ee142d4a171a6098092 - languageName: node - linkType: hard - -"@aws-sdk/util-endpoints@npm:3.980.0": - version: 3.980.0 - resolution: "@aws-sdk/util-endpoints@npm:3.980.0" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/url-parser": "npm:^4.2.8" - "@smithy/util-endpoints": "npm:^3.2.8" - tslib: "npm:^2.6.2" - checksum: 10/a61ec475660cc736960663f756970e07246a7684b762830e8b17ec0873dc5a4f9135fa6104219a2c790d22f30d36369ee19ade124b396d6f09a1139a878e656e - languageName: node - linkType: hard - -"@aws-sdk/util-format-url@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/util-format-url@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/querystring-builder": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/3d128ba22efc0d58406dd9e9503e62d75ae0dea22ed0276f9755acf598236918d0c2802947e0031ac924a14e8b21c387520e08515bedf56ee00fe83f4747b795 - languageName: node - linkType: hard - -"@aws-sdk/util-locate-window@npm:^3.0.0": - version: 3.957.0 - resolution: "@aws-sdk/util-locate-window@npm:3.957.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/ab9efda42115c605cd35710750d8fe55a832962d499c77d1218d3e9a127dfeec33342f35f15845dd7688833f7cdda0e190555719b42641eb75a4e76607bcb5e6 - languageName: node - linkType: hard - -"@aws-sdk/util-user-agent-browser@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.972.3" - dependencies: - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/types": "npm:^4.12.0" - bowser: "npm:^2.11.0" - tslib: "npm:^2.6.2" - checksum: 10/fb51d6ae56ba2a69a1239fc1f83a739c468c78ff678cf336b923273237e861b8ff4bfb296b7a250f5980dc2ef6741492a802432243313daf9a03a5332199f7aa - languageName: node - linkType: hard - -"@aws-sdk/util-user-agent-node@npm:^3.972.3": - version: 3.972.3 - resolution: "@aws-sdk/util-user-agent-node@npm:3.972.3" - dependencies: - "@aws-sdk/middleware-user-agent": "npm:^3.972.5" - "@aws-sdk/types": "npm:^3.973.1" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - peerDependencies: - aws-crt: ">=1.0.0" - peerDependenciesMeta: - aws-crt: - optional: true - checksum: 10/abeabdf825d9fbcc2e88c0ce6c47f15b29a8a0932e3106cd2637d0843897abca3b7f2eef757b31c82eb0ced0d733b84c9695ff260b950794bab9aac9807871b3 - languageName: node - linkType: hard - -"@aws-sdk/xml-builder@npm:^3.972.2": - version: 3.972.2 - resolution: "@aws-sdk/xml-builder@npm:3.972.2" - dependencies: - "@smithy/types": "npm:^4.12.0" - fast-xml-parser: "npm:5.2.5" - tslib: "npm:^2.6.2" - checksum: 10/d2f16b53520589fcc1d7720a290286790da94690f49c472afa7017b1250f98abcdb1d32d39b29d7a6c63542eb6808cb006702d5bd470365e86aef18d6dc76ea4 - languageName: node - linkType: hard - -"@aws/lambda-invoke-store@npm:^0.2.2": - version: 0.2.2 - resolution: "@aws/lambda-invoke-store@npm:0.2.2" - checksum: 10/18cd0cec90d9d865c9089218ef2220b0a7302a860c9a3f808b101386f569abc5ee11eb98a36947bed280a63308dd5df23c39e7b07fe9ac4f4ffcd0c4dce537c4 - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -16356,614 +15662,6 @@ __metadata: languageName: node linkType: hard -"@smithy/abort-controller@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/abort-controller@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/17d5beb1c86227ced459e6abbb03d6a3f205bd6f535a4bca2a10e9b4838292c533be78dbf39cdbf1f8f4af0c2fc3fec2f3081b3d4a1bf4e12a2a2aa52e298173 - languageName: node - linkType: hard - -"@smithy/chunked-blob-reader-native@npm:^4.2.1": - version: 4.2.1 - resolution: "@smithy/chunked-blob-reader-native@npm:4.2.1" - dependencies: - "@smithy/util-base64": "npm:^4.3.0" - tslib: "npm:^2.6.2" - checksum: 10/491cd1fbf74c53cc8c63abef1d9c0e93d1c0773db2c4458d4d3bd08217ea58872e413191b56259fd8081653ee07628e3ffcf7ff594d124378401fc3637794474 - languageName: node - linkType: hard - -"@smithy/chunked-blob-reader@npm:^5.2.0": - version: 5.2.0 - resolution: "@smithy/chunked-blob-reader@npm:5.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/c2f3b93343daba9a71e2f00fb93ae527a03c0adb6c6c6e194834bf4a67111e87f0694e2d9dd9b70bca87e9eb9da1d905d4450147e54e4cd27c6703dd98d58e0c - languageName: node - linkType: hard - -"@smithy/config-resolver@npm:^4.4.6": - version: 4.4.6 - resolution: "@smithy/config-resolver@npm:4.4.6" - dependencies: - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-config-provider": "npm:^4.2.0" - "@smithy/util-endpoints": "npm:^3.2.8" - "@smithy/util-middleware": "npm:^4.2.8" - tslib: "npm:^2.6.2" - checksum: 10/6440612a9e9a29b74f3420244f3e416d2c2ff0ed4956af323cd39eb4b8efe22a01e791e8cf465c5b0230a778a825290d6b935e3c6d4ca5a92336b48a2b2b4dbd - languageName: node - linkType: hard - -"@smithy/core@npm:^3.22.0": - version: 3.22.0 - resolution: "@smithy/core@npm:3.22.0" - dependencies: - "@smithy/middleware-serde": "npm:^4.2.9" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-stream": "npm:^4.5.10" - "@smithy/util-utf8": "npm:^4.2.0" - "@smithy/uuid": "npm:^1.1.0" - tslib: "npm:^2.6.2" - checksum: 10/2a10318b5503f02777a29c77578977ff427808edb98bb481d5d0ac770b99b662137abc89564f50ef92e4ef0a64366a3da4e9b8cd86ede13e76a694db1b4e6584 - languageName: node - linkType: hard - -"@smithy/credential-provider-imds@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/credential-provider-imds@npm:4.2.8" - dependencies: - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/url-parser": "npm:^4.2.8" - tslib: "npm:^2.6.2" - checksum: 10/f0d7abbe28a8244cacf65a453f132e38902e8e912b284b8371165b94ce6ae183acedc430d84ab466ef2d6930867f44d6aeaa4bb877e53a06a8f2dbd42c145d69 - languageName: node - linkType: hard - -"@smithy/eventstream-codec@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/eventstream-codec@npm:4.2.8" - dependencies: - "@aws-crypto/crc32": "npm:5.2.0" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-hex-encoding": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/45e027b320056dc82ce23928a09d29baa5d080c89008874f409c557228923ce216940990bbe53204d8628a0ca4d1e774cbb5aaceb4b5ba6237b89c108ce39a32 - languageName: node - linkType: hard - -"@smithy/eventstream-serde-browser@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/eventstream-serde-browser@npm:4.2.8" - dependencies: - "@smithy/eventstream-serde-universal": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/10aef5211bb360b67861f672084a1270caa8b5c1ab5ccbb388d507080387d65b714239e997e8851ec8a38082144ebca316af0db963b1aae15f5160c5c36a1315 - languageName: node - linkType: hard - -"@smithy/eventstream-serde-config-resolver@npm:^4.3.8": - version: 4.3.8 - resolution: "@smithy/eventstream-serde-config-resolver@npm:4.3.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/fbd4b1278c047a7b8bde7181a17c46ee17c93c8d907d54f8122312bed16a6ef835914962746ec4cb11154a09c9eec166e7ffd3bdc65af0a38a62ab7083902418 - languageName: node - linkType: hard - -"@smithy/eventstream-serde-node@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/eventstream-serde-node@npm:4.2.8" - dependencies: - "@smithy/eventstream-serde-universal": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/603840ac95222293b7b5db6201249b08c2dd9ee343a66fde5a5025b1f3bab130be6b4f6ddd7b657a440b422a2f16868a2f30553eb1a27aafabcf8a0aab1729c9 - languageName: node - linkType: hard - -"@smithy/eventstream-serde-universal@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/eventstream-serde-universal@npm:4.2.8" - dependencies: - "@smithy/eventstream-codec": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/814366a4184ed28e51edeeee43c46b3a8e7153d1136e0802e86c6ff9143c73bf6137617b67c7763d374ed921d673f54fd950bf0fdc09aebaf07977eeb0c60e63 - languageName: node - linkType: hard - -"@smithy/fetch-http-handler@npm:^5.3.9": - version: 5.3.9 - resolution: "@smithy/fetch-http-handler@npm:5.3.9" - dependencies: - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/querystring-builder": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-base64": "npm:^4.3.0" - tslib: "npm:^2.6.2" - checksum: 10/7e350c6a4f49e9c913367791f2fb48bc160ae60ad2a6f314baf384623aed2ee5b50996b4ffcc8ddf8abb0ba9489bb524dedb1769756431c45e3ab7bfc41b7994 - languageName: node - linkType: hard - -"@smithy/hash-blob-browser@npm:^4.2.9": - version: 4.2.9 - resolution: "@smithy/hash-blob-browser@npm:4.2.9" - dependencies: - "@smithy/chunked-blob-reader": "npm:^5.2.0" - "@smithy/chunked-blob-reader-native": "npm:^4.2.1" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/de9641b7b66085e35a2896304216419fb7f073609f12686d7df775b0df8c83066e778a757e664be37c07ed4c2f87cce7754878213a2e4cd6f80cc208e61aa42f - languageName: node - linkType: hard - -"@smithy/hash-node@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/hash-node@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - "@smithy/util-buffer-from": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/db765b8f338e4109aab1d7032175c74673bfedff10cae2241e91034efa42cf01a657f5c0494ef79fc9d7aa2da9ab01981c64583d0a736baf5e6b3038a69a0c1f - languageName: node - linkType: hard - -"@smithy/hash-stream-node@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/hash-stream-node@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/154583e9f39508aad8250d121bb6810a480db6428319b12a10465b83cc87246c74cbef65ec71953c7a80d626fb55e38506b294d93a082fabf9217be7c7d35cda - languageName: node - linkType: hard - -"@smithy/invalid-dependency@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/invalid-dependency@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/e1c1d0a654e096f74dfec32e48492075f4d96f7f3694a1c5b530c575e402eb605f381748f321ae7b491b97142d3bfbd55f269b1b3257dcc0d3aa38508e227e2b - languageName: node - linkType: hard - -"@smithy/is-array-buffer@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/is-array-buffer@npm:2.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/d366743ecc7a9fc3bad21dbb3950d213c12bdd4aeb62b1265bf6cbe38309df547664ef3e51ab732e704485194f15e89d361943b0bfbe3fe1a4b3178b942913cc - languageName: node - linkType: hard - -"@smithy/is-array-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/is-array-buffer@npm:4.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/fdc097ce6a8b241565e2d56460ec289730bcd734dcde17c23d1eaaa0996337f897217166276a3fd82491fe9fd17447aadf62e8d9056b3d2b9daf192b4b668af9 - languageName: node - linkType: hard - -"@smithy/md5-js@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/md5-js@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/bc5478f5918c9c9bb7f6f3b62c2a374b20c3f7e0a01df25edf1f8b0832778a0625d69df50bf01c9434e9d8002561c28bc20a2d151cfc7a89d157a79bd900e199 - languageName: node - linkType: hard - -"@smithy/middleware-content-length@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/middleware-content-length@npm:4.2.8" - dependencies: - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/9077c99f263843d347c847057ba3f7c270a8f71d96018f123fd78f1a0439f076e5ae989e7ce83e158f94b45afc7e8665f67d33e4c2cb66d7bbb88495ae9f1785 - languageName: node - linkType: hard - -"@smithy/middleware-endpoint@npm:^4.4.12": - version: 4.4.12 - resolution: "@smithy/middleware-endpoint@npm:4.4.12" - dependencies: - "@smithy/core": "npm:^3.22.0" - "@smithy/middleware-serde": "npm:^4.2.9" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - "@smithy/url-parser": "npm:^4.2.8" - "@smithy/util-middleware": "npm:^4.2.8" - tslib: "npm:^2.6.2" - checksum: 10/cd45ae6da1cb327fe2ff79ca1a5635c43ca6c47cdc42dc3c1103bcfc9b61417d444a8a927bf9a3f440ce7b8390520ccb606d72cd77f361433e4f24c65f94c533 - languageName: node - linkType: hard - -"@smithy/middleware-retry@npm:^4.4.29": - version: 4.4.29 - resolution: "@smithy/middleware-retry@npm:4.4.29" - dependencies: - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/service-error-classification": "npm:^4.2.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-retry": "npm:^4.2.8" - "@smithy/uuid": "npm:^1.1.0" - tslib: "npm:^2.6.2" - checksum: 10/a95d40ea5d5c44c9815726b803d742975caa49aafbc9c321af5f1dafc1c3810b38cb2a83791be36ecfae50bb6ddbd9deac1b99fa4b81acbd7a3a2217dbfa9387 - languageName: node - linkType: hard - -"@smithy/middleware-serde@npm:^4.2.9": - version: 4.2.9 - resolution: "@smithy/middleware-serde@npm:4.2.9" - dependencies: - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/490e9ab6ce6664812e30975d3f24d769c8ba59f153c97a5095516f8fd22ed6d948cd4838cfdb253b020b3ec8914b4ec3cb31f1d6ca84ece7639381d5dec6c463 - languageName: node - linkType: hard - -"@smithy/middleware-stack@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/middleware-stack@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/c4b8dc4e466e31e4adc36a52af5e7f5bdc9adf3cc31e825947a2f73f5e1beb5ef87b72624427e6f3a18951407878d7f0ef33990c210aa7df5143c028f0ef8740 - languageName: node - linkType: hard - -"@smithy/node-config-provider@npm:^4.3.8": - version: 4.3.8 - resolution: "@smithy/node-config-provider@npm:4.3.8" - dependencies: - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/shared-ini-file-loader": "npm:^4.4.3" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/e954b98ad121e76174453bf67bf9824b661de61865d3e92e845d6e0656b3d8c41ebc90a176428d3732a14dd8cfe5795644864d17470a5af37599c2c4b3c221fd - languageName: node - linkType: hard - -"@smithy/node-http-handler@npm:^4.4.8": - version: 4.4.8 - resolution: "@smithy/node-http-handler@npm:4.4.8" - dependencies: - "@smithy/abort-controller": "npm:^4.2.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/querystring-builder": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/f5df30b2dc307c36a866104c415af2b776ad28821948f4391ae18bd62f66a99886530b0ff9c02f7389bcbda1dcc325f5818d6edf9cf1bea361a81f40892cadac - languageName: node - linkType: hard - -"@smithy/property-provider@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/property-provider@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/d50f51bf029f72ec3679c7945cbb77f71d53fa5f53a20adcbc0ab25f53587add46d1ed1dd90becb1bdf0c97c9caf7f8a45d868eefe3951a4e68bc3ce5ed1eb29 - languageName: node - linkType: hard - -"@smithy/protocol-http@npm:^5.3.8": - version: 5.3.8 - resolution: "@smithy/protocol-http@npm:5.3.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/6465375d9feff2c2718e5b30d358f3d63f007574b2338c6b08dde11d11a98371697b9ec047455fa71be6ede9770e7e53ee5d9715ed7033dbfb825ec4d029066e - languageName: node - linkType: hard - -"@smithy/querystring-builder@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/querystring-builder@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - "@smithy/util-uri-escape": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/13bd560936d31f51006174f962260526c21df1cdb821f83cc3f7e6424c1a37f2b6b76a92bef1241174eebbdd5ef06f050752460ad638f7814f23f499e0a847fa - languageName: node - linkType: hard - -"@smithy/querystring-parser@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/querystring-parser@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/26e5a3fc8d1623980f9a03662b6b2349a4a4e6f0ecb9af4df9f11a2cc83a58d4ef3571d104e5ff1a10973a4e297b3aa8327f261d647ffc6f5ee871008a740580 - languageName: node - linkType: hard - -"@smithy/service-error-classification@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/service-error-classification@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - checksum: 10/ffcbaa6fa3536642dc03f3c7feb762a3b4acfa5d45ff74e401634f472549fce2608a5b1ebd339de5fc0ba2e0f6296b5fa8e49258cb1b675aa298aed631728542 - languageName: node - linkType: hard - -"@smithy/shared-ini-file-loader@npm:^4.4.3": - version: 4.4.3 - resolution: "@smithy/shared-ini-file-loader@npm:4.4.3" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/70cf7db0e24768d5e6a019de29d194ca4516e9177cbd9cd97ce7800889ee2bd3d8cfd71958d11cd026f79223cb34c64176234443d464cf6146562e0385f7daea - languageName: node - linkType: hard - -"@smithy/signature-v4@npm:^5.3.8": - version: 5.3.8 - resolution: "@smithy/signature-v4@npm:5.3.8" - dependencies: - "@smithy/is-array-buffer": "npm:^4.2.0" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-hex-encoding": "npm:^4.2.0" - "@smithy/util-middleware": "npm:^4.2.8" - "@smithy/util-uri-escape": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/88bd0b507bf1a567519208d5b5fb923142bf63bd9b7bfd8b0d4485a8225a80c4274956770127ef471ace96dbb00f1e0bee0bafeb365c5f5346e5419e6ed882fc - languageName: node - linkType: hard - -"@smithy/smithy-client@npm:^4.11.1": - version: 4.11.1 - resolution: "@smithy/smithy-client@npm:4.11.1" - dependencies: - "@smithy/core": "npm:^3.22.0" - "@smithy/middleware-endpoint": "npm:^4.4.12" - "@smithy/middleware-stack": "npm:^4.2.8" - "@smithy/protocol-http": "npm:^5.3.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-stream": "npm:^4.5.10" - tslib: "npm:^2.6.2" - checksum: 10/b72ed4deea2fea948e89b026d319d85380a23bce7a7f6690769a2615ba5d989656a43f6a5aa85dbbb35b1e7d6c3ffab0546fb97daa506b3d87f6b9d929da1f6e - languageName: node - linkType: hard - -"@smithy/types@npm:^4.12.0": - version: 4.12.0 - resolution: "@smithy/types@npm:4.12.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/7fe734b4cae1ae3a5c3f8a0aefae072530026917436a5db699d2e27e3518cde4ba4ffe001ef7c45e4a87a02bdae8eabb67b82e6db80153eaf41776901718aa62 - languageName: node - linkType: hard - -"@smithy/url-parser@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/url-parser@npm:4.2.8" - dependencies: - "@smithy/querystring-parser": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/8e99b893502f219e5bd9c17f6f974a433f3e56c6dc899cb753281c7701c19126f202766dcee69c4e5ecb1b941daa68bc5d6ea603dd5121bce0de5135268664d4 - languageName: node - linkType: hard - -"@smithy/util-base64@npm:^4.3.0": - version: 4.3.0 - resolution: "@smithy/util-base64@npm:4.3.0" - dependencies: - "@smithy/util-buffer-from": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/87065ca13e3745858e0bb0ab6374433b258c378ee2a5ef865b74f6a4208c56db7db2b9ee5f888e021de0107fae49e9957662c4c6847fe10529e2f6cc882426b4 - languageName: node - linkType: hard - -"@smithy/util-body-length-browser@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-body-length-browser@npm:4.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/deeb689b52652651c11530a324e07725805533899215ad1f93c5e9a14931443e22b313491a3c2a6d7f61d6dd1e84f9154d0d32de62bf61e0bd8e6ab7bf5f81ed - languageName: node - linkType: hard - -"@smithy/util-body-length-node@npm:^4.2.1": - version: 4.2.1 - resolution: "@smithy/util-body-length-node@npm:4.2.1" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/efb1333d35120124ec0c751b7b7d5657eb9ad6d0bf6171ff61fde2504639883d36e9562613c70eca623b726193b22601c8ff60e40a8156102d4c5b12fae222f8 - languageName: node - linkType: hard - -"@smithy/util-buffer-from@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-buffer-from@npm:2.2.0" - dependencies: - "@smithy/is-array-buffer": "npm:^2.2.0" - tslib: "npm:^2.6.2" - checksum: 10/53253e4e351df3c4b7907dca48a0a6ceae783e98a8e73526820b122b3047a53fd127c19f4d8301f68d852011d821da519da783de57e0b22eed57c4df5b90d089 - languageName: node - linkType: hard - -"@smithy/util-buffer-from@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-buffer-from@npm:4.2.0" - dependencies: - "@smithy/is-array-buffer": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/6a81e658554d7123fe089426a840b5e691aee4aa4f0d72b79af19dcf57ccb212dca518acb447714792d48c2dc99bda5e0e823dab05e450ee2393146706d476f9 - languageName: node - linkType: hard - -"@smithy/util-config-provider@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-config-provider@npm:4.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/d65f36401c7a085660cf201a1b317d271e390258b619179fff88248c2db64fc35e6c62fe055f1e55be8935b06eb600379824dabf634fb26d528f54fe60c9d77b - languageName: node - linkType: hard - -"@smithy/util-defaults-mode-browser@npm:^4.3.28": - version: 4.3.28 - resolution: "@smithy/util-defaults-mode-browser@npm:4.3.28" - dependencies: - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/175aa34d207a66a2fb500a1ef68b8a89455e11410fbb2687eba099efb3ededb505144811ac6ed7df1fe29bce3758bf2c748b1b85f5ee8b6f7d4efef61553ff53 - languageName: node - linkType: hard - -"@smithy/util-defaults-mode-node@npm:^4.2.31": - version: 4.2.31 - resolution: "@smithy/util-defaults-mode-node@npm:4.2.31" - dependencies: - "@smithy/config-resolver": "npm:^4.4.6" - "@smithy/credential-provider-imds": "npm:^4.2.8" - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/property-provider": "npm:^4.2.8" - "@smithy/smithy-client": "npm:^4.11.1" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/fd5a7d8789b898ca9c17805eb44a62e5b81c35dc4b0823e3d68f70ba0a5a605c36a2c4c528259724293735953b64f53f4184de7e34ac642d2a990b05eb979e20 - languageName: node - linkType: hard - -"@smithy/util-endpoints@npm:^3.2.8": - version: 3.2.8 - resolution: "@smithy/util-endpoints@npm:3.2.8" - dependencies: - "@smithy/node-config-provider": "npm:^4.3.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/65ea9b1d5abaa944290d6cc4106f74909dafb832616187c17b6c6705f4cb3aa9ea33068595cf161418020a01724716e3c3e1534e78983e92a656f3b85cac02bf - languageName: node - linkType: hard - -"@smithy/util-hex-encoding@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-hex-encoding@npm:4.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/478773d73690e39167b67481116c4fd47cecfc97c3a935d88db9271fb0718627bec1cbc143efbf0cd49d1ac417bde7e76aa74139ea07e365b51e66797f63a45d - languageName: node - linkType: hard - -"@smithy/util-middleware@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/util-middleware@npm:4.2.8" - dependencies: - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/a675f1968ad4a674cc70833be14e8f0e99b09626db9c5764e1d92c76e663d83ba64af4aac5d03112726436cad045cc817d19a71addc5aca6d363b1964ff51d31 - languageName: node - linkType: hard - -"@smithy/util-retry@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/util-retry@npm:4.2.8" - dependencies: - "@smithy/service-error-classification": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/c725368bafc63cc54a2fad528d5667998986699ca87c87c30e12354f45008b0664f7d1b2afb0e310190227a1e99aa4c44dcb27e8663431ca3b37659c44ec339b - languageName: node - linkType: hard - -"@smithy/util-stream@npm:^4.5.10": - version: 4.5.10 - resolution: "@smithy/util-stream@npm:4.5.10" - dependencies: - "@smithy/fetch-http-handler": "npm:^5.3.9" - "@smithy/node-http-handler": "npm:^4.4.8" - "@smithy/types": "npm:^4.12.0" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-buffer-from": "npm:^4.2.0" - "@smithy/util-hex-encoding": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/7d8fc4f86fc43edba5124836a7701cacacd65aa0f3a917faba4febcc091055c2be176b3de9bdacbcff5b7e8a97ecd35c66e38fd92743de385fd9774bdbdcc42f - languageName: node - linkType: hard - -"@smithy/util-uri-escape@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-uri-escape@npm:4.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/a838a3afe557d7087d4500735c79d5da72e0cd5a08f95d1a1c450ba29d9cd85c950228eedbd9b2494156f4eb8658afb0a9a5bd2df3fc4f297faed886c396242b - languageName: node - linkType: hard - -"@smithy/util-utf8@npm:^2.0.0": - version: 2.3.0 - resolution: "@smithy/util-utf8@npm:2.3.0" - dependencies: - "@smithy/util-buffer-from": "npm:^2.2.0" - tslib: "npm:^2.6.2" - checksum: 10/c766ead8dac6bc6169f4cac1cc47ef7bd86928d06255148f9528228002f669c8cc49f78dc2b9ba5d7e214d40315024a9e32c5c9130b33e20f0fe4532acd0dff5 - languageName: node - linkType: hard - -"@smithy/util-utf8@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-utf8@npm:4.2.0" - dependencies: - "@smithy/util-buffer-from": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/d49f58fc6681255eecc3dee39c657b80ef8a4c5617e361bdaf6aaa22f02e378622376153cafc9f0655fb80162e88fc98bbf459f8dd5ba6d7c4b9a59e6eaa05f8 - languageName: node - linkType: hard - -"@smithy/util-waiter@npm:^4.2.8": - version: 4.2.8 - resolution: "@smithy/util-waiter@npm:4.2.8" - dependencies: - "@smithy/abort-controller": "npm:^4.2.8" - "@smithy/types": "npm:^4.12.0" - tslib: "npm:^2.6.2" - checksum: 10/d492ed07fc9b1147660d99b142c4db150d730f2155ba3027363894c97c3d6a539cb69ae6952cf25cb5f79b870e4ce13a30d8fcd7346b3a358d223ae1b080188a - languageName: node - linkType: hard - -"@smithy/uuid@npm:^1.1.0": - version: 1.1.0 - resolution: "@smithy/uuid@npm:1.1.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/fe77b1cebbbf2d541ee2f07eec6d4573af16e08dd3228758f59dcbe85a504112cefe81b971818cf39e2e3fa0ed1fcc61d392cddc50fca13d9dc9bd835e366db0 - languageName: node - linkType: hard - "@socket.io/component-emitter@npm:~3.1.0": version: 3.1.2 resolution: "@socket.io/component-emitter@npm:3.1.2" @@ -17778,6 +16476,15 @@ __metadata: languageName: node linkType: hard +"@types/aws4@npm:^1.11.6": + version: 1.11.6 + resolution: "@types/aws4@npm:1.11.6" + dependencies: + "@types/node": "npm:*" + checksum: 10/7b75159338526f27ce55530bba7addd82152acf5db743728f8006a23cfab730f33e4d2bb788cc279a36947a5ef25d23ed0c2484639f2ffaf04e8d3d27911da3a + languageName: node + linkType: hard + "@types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -20619,6 +19326,13 @@ __metadata: languageName: node linkType: hard +"aws4@npm:^1.13.2": + version: 1.13.2 + resolution: "aws4@npm:1.13.2" + checksum: 10/290b9f84facbad013747725bfd8b4c42d0b3b04b5620d8418f0219832ef95a7dc597a4af7b1589ae7fce18bacde96f40911c3cda36199dd04d9f8e01f72fa50a + languageName: node + linkType: hard + "axios@npm:^1.8.3": version: 1.12.2 resolution: "axios@npm:1.12.2" @@ -20868,13 +19582,6 @@ __metadata: languageName: node linkType: hard -"bowser@npm:^2.11.0": - version: 2.11.0 - resolution: "bowser@npm:2.11.0" - checksum: 10/ef46500eafe35072455e7c3ae771244e97827e0626686a9a3601c436d16eb272dad7ccbd49e2130b599b617ca9daa67027de827ffc4c220e02f63c84b69a8751 - languageName: node - linkType: hard - "boxen@npm:7.0.0": version: 7.0.0 resolution: "boxen@npm:7.0.0" @@ -25456,17 +24163,6 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:5.2.5": - version: 5.2.5 - resolution: "fast-xml-parser@npm:5.2.5" - dependencies: - strnum: "npm:^2.1.0" - bin: - fxparser: src/cli/cli.js - checksum: 10/305017cff6968a34cbac597317be1516e85c44f650f30d982c84f8c30043e81fd38d39a8810d570136c921399dd43b9ac4775bdfbbbcfee96456f3c086b48bdd - languageName: node - linkType: hard - "fast-xml-parser@npm:^5.3.4": version: 5.3.4 resolution: "fast-xml-parser@npm:5.3.4" @@ -35101,6 +33797,13 @@ __metadata: languageName: node linkType: hard +"s3mini@npm:^0.9.1": + version: 0.9.1 + resolution: "s3mini@npm:0.9.1" + checksum: 10/cefb0caac2d24119193c96bcf798ad6a2a7c89cef9a26638d74a304d39313c669090b15e72fdb6b36be1fd1eae55f1388e3085cc0e52f67a269572546cb8d26d + languageName: node + linkType: hard + "safe-buffer@npm:@nolyfill/safe-buffer@^1": version: 1.0.44 resolution: "@nolyfill/safe-buffer@npm:1.0.44"