s3mini 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/consts.ts CHANGED
@@ -13,6 +13,7 @@ export const DEFAULT_REQUEST_SIZE_IN_BYTES = 8 * 1024 * 1024;
13
13
 
14
14
  // Headers
15
15
  export const HEADER_AMZ_CONTENT_SHA256 = 'x-amz-content-sha256';
16
+ export const HEADER_AMZ_CHECKSUM_SHA256 = 'x-amz-checksum-sha256';
16
17
  export const HEADER_AMZ_DATE = 'x-amz-date';
17
18
  export const HEADER_HOST = 'host';
18
19
  export const HEADER_AUTHORIZATION = 'authorization';
@@ -32,7 +33,6 @@ export const ERROR_UPLOAD_ID_REQUIRED = `${ERROR_PREFIX}uploadId must be a non-e
32
33
  export const ERROR_PARTS_REQUIRED = `${ERROR_PREFIX}parts must be a non-empty array`;
33
34
  export const ERROR_INVALID_PART = `${ERROR_PREFIX}Each part must have a partNumber (number) and ETag (string)`;
34
35
  export const ERROR_DATA_BUFFER_REQUIRED = `${ERROR_PREFIX}data must be a Buffer or string`;
35
- // const ERROR_PATH_REQUIRED = `${ERROR_PREFIX}path must be a string`;
36
36
  export const ERROR_PREFIX_TYPE = `${ERROR_PREFIX}prefix must be a string`;
37
37
  export const ERROR_MAX_KEYS_TYPE = `${ERROR_PREFIX}maxKeys must be a positive integer`;
38
38
  export const ERROR_DELIMITER_REQUIRED = `${ERROR_PREFIX}delimiter must be a string`;
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- import { S3mini, s3mini } from './S3.js';
3
+ import { S3mini } from './S3.js';
4
4
  import { sanitizeETag, runInBatches } from './utils.js';
5
5
 
6
6
  // Export the S3 class as default export and named export
7
- export { S3mini, s3mini, sanitizeETag, runInBatches };
7
+ export { S3mini, sanitizeETag, runInBatches };
8
8
  export default S3mini;
9
9
 
10
10
  // Re-export types
package/src/types.ts CHANGED
@@ -14,18 +14,8 @@ export interface SSECHeaders {
14
14
  'x-amz-server-side-encryption-customer-key-md5': string;
15
15
  }
16
16
 
17
- export interface Crypto {
18
- createHmac: (
19
- algorithm: string,
20
- key: Buffer | string,
21
- ) => {
22
- update: (data: Buffer | string) => { digest: (encoding?: string) => string | Buffer };
23
- digest: (encoding?: string) => string | Buffer;
24
- };
25
- createHash: (algorithm: string) => {
26
- update: (data: Buffer | string) => { digest: (encoding?: string) => string | Buffer };
27
- digest: (encoding?: string) => string | Buffer;
28
- };
17
+ export interface AWSHeaders {
18
+ [k: `x-amz-${string}`]: string;
29
19
  }
30
20
 
31
21
  export interface Logger {
@@ -72,7 +62,7 @@ export interface ListMultipartUploadSuccess {
72
62
  key: string;
73
63
  uploadId: string;
74
64
  size?: number;
75
- mtime?: Date | undefined;
65
+ mtime?: Date;
76
66
  etag?: string;
77
67
  eTag?: string; // for backward compatibility
78
68
  parts: UploadPart[];
@@ -107,3 +97,62 @@ export interface XmlMap {
107
97
  [key: string]: XmlValue | XmlValue[]; // one or many children
108
98
  [key: number]: XmlValue | XmlValue[]; // allow numeric keys
109
99
  }
100
+
101
+ export interface CopyObjectOptions {
102
+ /**
103
+ * Specifies whether the metadata is copied from the source object or replaced with metadata provided in the request.
104
+ * Valid values: 'COPY' | 'REPLACE'
105
+ * Default: 'COPY'
106
+ */
107
+ metadataDirective?: 'COPY' | 'REPLACE';
108
+
109
+ /**
110
+ * Metadata to be set on the destination object when metadataDirective is 'REPLACE'.
111
+ * Keys can be provided with or without 'x-amz-meta-' prefix.
112
+ */
113
+ metadata?: Record<string, string>;
114
+ contentType?: string;
115
+
116
+ /**
117
+ * Storage class for the destination object.
118
+ * Valid values: 'STANDARD' | 'REDUCED_REDUNDANCY' | 'STANDARD_IA' | 'ONEZONE_IA' | 'INTELLIGENT_TIERING' | 'GLACIER' | 'DEEP_ARCHIVE' | 'GLACIER_IR'
119
+ */
120
+ storageClass?: string;
121
+
122
+ /**
123
+ * Specifies whether the object tag-set is copied from the source object or replaced with tag-set provided in the request.
124
+ * Valid values: 'COPY' | 'REPLACE'
125
+ */
126
+ taggingDirective?: 'COPY' | 'REPLACE';
127
+
128
+ /**
129
+ * If the bucket is configured as a website, redirects requests for this object to another object or URL.
130
+ */
131
+ websiteRedirectLocation?: string;
132
+
133
+ /**
134
+ * Server-Side Encryption with Customer-Provided Keys headers for the source object.
135
+ * Should include:
136
+ * - x-amz-copy-source-server-side-encryption-customer-algorithm
137
+ * - x-amz-copy-source-server-side-encryption-customer-key
138
+ * - x-amz-copy-source-server-side-encryption-customer-key-MD5
139
+ */
140
+ sourceSSECHeaders?: Record<string, string | number>;
141
+ destinationSSECHeaders?: SSECHeaders;
142
+ additionalHeaders?: Record<string, string | number>;
143
+ }
144
+
145
+ export interface CopyObjectResult {
146
+ etag: string;
147
+ lastModified?: Date;
148
+ }
149
+
150
+ /**
151
+ * Where Buffer is available, e.g. when @types/node is loaded, we want to use it.
152
+ * But it should be excluded in other environments (e.g. Cloudflare).
153
+ */
154
+ export type MaybeBuffer = typeof globalThis extends { Buffer?: infer B }
155
+ ? B extends new (...a: unknown[]) => unknown
156
+ ? InstanceType<B>
157
+ : ArrayBuffer | Uint8Array
158
+ : ArrayBuffer | Uint8Array;
package/src/utils.ts CHANGED
@@ -1,35 +1,80 @@
1
1
  'use strict';
2
- import type { Crypto, XmlValue, XmlMap, ListBucketResponse, ErrorWithCode } from './types.js';
3
- declare const crypto: Crypto;
2
+ import type { XmlValue, XmlMap, ListBucketResponse, ErrorWithCode } from './types.js';
4
3
 
5
- // Initialize crypto functions - this is needed for environments where `crypto` is not available globally
6
- // e.g., in Cloudflare Workers or other non-Node.js environments with nodejs_flags enabled.
7
- const _createHmac: Crypto['createHmac'] = crypto.createHmac || (await import('node:crypto')).createHmac;
8
- const _createHash: Crypto['createHash'] = crypto.createHash || (await import('node:crypto')).createHash;
4
+ const ENCODR = new TextEncoder();
5
+ const chunkSize = 0x8000; // 32KB chunks
6
+ const HEXS = '0123456789abcdef';
7
+
8
+ export const getByteSize = (data: unknown): number => {
9
+ if (typeof data === 'string') {
10
+ return ENCODR.encode(data).byteLength;
11
+ }
12
+ if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
13
+ return data.byteLength;
14
+ }
15
+ if (data instanceof Blob) {
16
+ return data.size;
17
+ }
18
+ throw new Error('Unsupported data type');
19
+ };
9
20
 
10
21
  /**
11
- * Hash content using SHA-256
12
- * @param {string|Buffer} content – data to hash
13
- * @returns {string} Hex encoded hash
22
+ * Turn a raw ArrayBuffer into its hexadecimal representation.
23
+ * @param {ArrayBuffer} buffer The raw bytes.
24
+ * @returns {string} Hexadecimal string
14
25
  */
15
- export const hash = (content: string | Buffer): string => {
16
- return _createHash('sha256').update(content).digest('hex') as string;
26
+ export const hexFromBuffer = (buffer: ArrayBuffer): string => {
27
+ const bytes = new Uint8Array(buffer);
28
+ let hex = '';
29
+ for (const byte of bytes) {
30
+ hex += HEXS[byte >> 4]! + HEXS[byte & 0x0f]!;
31
+ }
32
+ return hex;
17
33
  };
18
34
 
19
- export const md5base64 = (data: string | Buffer): string => {
20
- return _createHash('md5').update(data).digest('base64') as string;
35
+ /**
36
+ * Turn a raw ArrayBuffer into its base64 representation.
37
+ * @param {ArrayBuffer} buffer The raw bytes.
38
+ * @returns {string} Base64 string
39
+ */
40
+ export const base64FromBuffer = (buffer: ArrayBuffer): string => {
41
+ const bytes = new Uint8Array(buffer);
42
+ let result = '';
43
+ for (let i = 0; i < bytes.length; i += chunkSize) {
44
+ const chunk = bytes.subarray(i, i + chunkSize);
45
+ result += btoa(String.fromCharCode.apply(null, chunk as unknown as number[]));
46
+ }
47
+ return result;
21
48
  };
22
49
 
23
50
  /**
24
- * Compute HMAC-SHA-256 of arbitrary data and return a hex string.
25
- * @param {string|Buffer} key – secret key
26
- * @param {string|Buffer} content – data to authenticate
27
- * @param {BufferEncoding} [encoding='hex'] – hex | base64 | …
28
- * @returns {string | Buffer} hex encoded HMAC
51
+ * Compute SHA-256 hash of arbitrary string data.
52
+ * @param {string} content The content to be hashed.
53
+ * @returns {ArrayBuffer} The raw hash
29
54
  */
30
- export const hmac = (key: string | Buffer, content: string | Buffer, encoding?: 'hex' | 'base64'): string | Buffer => {
31
- const mac = _createHmac('sha256', key).update(content);
32
- return encoding ? mac.digest(encoding) : mac.digest();
55
+ export const sha256 = async (content: string): Promise<ArrayBuffer> => {
56
+ const data = ENCODR.encode(content);
57
+
58
+ return await globalThis.crypto.subtle.digest('SHA-256', data);
59
+ };
60
+
61
+ /**
62
+ * Compute HMAC-SHA-256 of arbitrary data.
63
+ * @param {string|ArrayBuffer} key The key used to sign the content.
64
+ * @param {string} content The content to be signed.
65
+ * @returns {ArrayBuffer} The raw signature
66
+ */
67
+ export const hmac = async (key: string | ArrayBuffer, content: string): Promise<ArrayBuffer> => {
68
+ const secret = await globalThis.crypto.subtle.importKey(
69
+ 'raw',
70
+ typeof key === 'string' ? ENCODR.encode(key) : key,
71
+ { name: 'HMAC', hash: 'SHA-256' },
72
+ false,
73
+ ['sign'],
74
+ );
75
+ const data = ENCODR.encode(content);
76
+
77
+ return await globalThis.crypto.subtle.sign('HMAC', secret, data);
33
78
  };
34
79
 
35
80
  /**
@@ -45,7 +90,7 @@ export const sanitizeETag = (etag: string): string => {
45
90
  '&QUOT;': '',
46
91
  '&#x00022': '',
47
92
  };
48
- return etag.replace(/^("|&quot;|&#34;)|("|&quot;|&#34;)$/g, m => replaceChars[m] as string);
93
+ return etag.replace(/(^("|&quot;|&#34;))|(("|&quot;|&#34;)$)/g, m => replaceChars[m] as string);
49
94
  };
50
95
 
51
96
  const entityMap = {