@focus-at/zs3 0.0.14 → 0.0.15

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/README.md CHANGED
@@ -1,49 +1,45 @@
1
- # ZeroS3
1
+ # ZeroS3 (zs3)
2
2
  ![GitHub License](https://img.shields.io/github/license/focus-at/zs3)
3
3
  ![GitHub Stars](https://img.shields.io/github/stars/focus-at/zs3)
4
4
 
5
- An ultra-lightweight (~3 KB) and high-performance client for S3-compatible cloud storage (AWS S3, MinIO, Cloudflare R2, Backblaze B2), built purely on modern Web Standards (`fetch`, `crypto.subtle`).
5
+ > ⚠️ **Status: WIP (Work in Progress)**
6
+ > This project is currently under active development (**WHIP**). The public API is highly experimental, subject to breaking changes, and the specification is still stabilizing.
6
7
 
7
- **Zero external dependencies.** Engineered for custom JS runtimes, Edge functions, Serverless environments, or any setup highly sensitive to bundle size and cold-start times.
8
+ ## Overview
9
+ **ZeroS3 (zs3)** is an ultra-lightweight (~3 KB), zero-dependency S3 client designed for performance-critical environments, edge workers, and client-side integration (**BYOS3** model). Built purely on modern Web Standards (`fetch`, `crypto.subtle`), it provides lightning-fast execution for AWS S3, MinIO, Cloudflare R2, and Backblaze B2.
8
10
 
9
- ## Installation
10
-
11
- ```js
12
- import { zs3 } from 'zeros3';
13
- ```
11
+ ## Features
12
+ - **Zero Dependencies:** Built entirely on native Web APIs (`fetch`, `crypto.subtle`).
13
+ - **Unified API:** One simple `execute()` method returning a native Web `Response`.
14
+ - **Drop-in Web Compatibility:** Seamlessly stream files straight into your web server responses or client-side components.
14
15
 
15
16
  ## Quick Start
16
17
 
17
- ```js
18
- const storage = zs3({
19
- endpoint: '127.0.0.1:9000',
20
- accessKeyId: 'your-access-key-id',
21
- secretAccessKey: 'your-secret-access-key',
22
- bucket: 'production-assets'
23
- });
18
+ ```typescript
19
+ import type { S3Client } from '@focus-at/zs3';
20
+ import { zs3 } from '@focus-at/zs3';
24
21
 
25
- // 1. Upload a file (String, Buffer, or Web Stream)
26
- await storage.put('bundles/app.tar', tarStream, {
27
- headers: { contentType: 'application/x-tar' }
22
+ const s3 = zs3({
23
+ endpoint: "s3.amazonaws.com",
24
+ accessKeyId: "YOUR_ACCESS_KEY",
25
+ secretAccessKey: "YOUR_SECRET_KEY",
26
+ bucket: "my-bucket",
27
+ region: "us-east-1"
28
28
  });
29
29
 
30
- // 2. Download a file (returns fetch Response, stream is in .body)
31
- const res = await storage.get('bundles/app.tar');
32
- const downloadStream = res.body;
30
+ // 1. List objects (Returns JSON Response)
31
+ const listRes = await s3.execute({ method: "GET" });
32
+ const data = await listRes.json();
33
+ console.log(data.Contents); // Array of files or undefined if empty
33
34
 
34
- // 3. List files (pass an object containing a prefix)
35
- const files = await storage.get({ prefix: 'bundles/' });
36
- console.log(files.contents);
35
+ // 2. Download a file (Proxy straight to your web framework Response)
36
+ const fileRes = await s3.execute({ method: "GET", key: "videos/movie.mp4" });
37
+ return new Response(fileRes.body, { headers: fileRes.headers });
37
38
 
38
- // 4. Retrieve metadata (HEAD request)
39
- const meta = await storage.stat('bundles/app.tar');
40
- console.log(meta.size, meta['x-amz-meta-version']);
41
-
42
- // 5. Delete an object
43
- await storage.del('bundles/app.tar');
39
+ // 3. Upload an object
40
+ await s3.execute({
41
+ method: "PUT",
42
+ key: "uploads/image.png",
43
+ body: fileBuffer
44
+ });
44
45
  ```
45
-
46
- ## Features
47
- - **Closure-Based Design (No `this`)**: Every single client method can be safely destructured and decoupled without losing context: `const { get, put } = zs3(opts);`.
48
- - **Inlined Micro XML Parser**: Seamlessly converts S3 object listings without bloating your `node_modules` or running a heavy general-purpose parser.
49
- - **Robust AWS v4 Signatures**: Automatically manages query parameter alphabetical sorting, whitespace trimming, and strict URI hex-encoding under the hood.
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ var l="HMAC",G="SHA-256",T="AWS4",U=[T,l,"SHA256"].join("-"),Q=/[:-]|\.\d{3}/g,X=/<(\w[-\w]*)(?:\/|[^>]*>((?:(?!<\1)[\s\S])*)<\/\1)>/gm,J=/\B[A-Z]/g,Y=/[!'()*]/g,Z=/^\/*/,I=new TextEncoder;var{subtle:m}=crypto,h=(e)=>e instanceof ArrayBuffer?e:e?.buffer?.constructor===ArrayBuffer?e.buffer:I.encode(e==null?"":`${e}`).buffer,K=(e)=>Array.from(new Uint8Array(e)).map((s)=>s.toString(16).padStart(2,"0")).join(""),g=async(e,s)=>{let a=await m.importKey("raw",h(e),{name:l,hash:G},!1,["sign"]);return new Uint8Array(await m.sign(l,a,I.encode(s)))},H=async(e)=>K(await m.digest(G,h(e))),P=(e)=>e.replace(J,"-$&").toLowerCase(),L=(e,s,a)=>{if(s)Object.keys(s).forEach((t)=>{let r=P(a+t);if(s[t]&&typeof s[t]!=="object")e[r]=`${s[t]}`});return e},F=(e)=>encodeURIComponent(e).replace(Y,(s)=>`%${s.charCodeAt(0).toString(16).toUpperCase()}`),A=(e)=>{let s={"":"",true:!0,false:!1},a=[...e.matchAll(X)];return a.length?a.reduce((t,[,r="",i=""])=>{let o=s[i]??(isNaN(+i)?A(i):+i);return t[r]=Array.isArray(t[r])?[...t[r],o]:t[r]!=null?[t[r],o]:r==="Contents"?[o]:o,t},{}):e};function V(e){let s={region:"us-east-1",bucket:"",...e};return{execute:async(t,r={})=>{let{method:i}=t,o="body"in t?t.body:null,b=t.bucket!==void 0?t.bucket:s.bucket,w="key"in t&&t.key?t.key.replace(Z,""):"",x=b?`/${b}/${w}`:`/${w}`,N=i==="GET"&&!("key"in t),y=Object.entries({...r.query,...N&&{listType:2,maxKeys:t.maxKeys||1000,prefix:t.prefix||""}}).sort().map(([n,p])=>`${P(n)}=${F(String(p??""))}`).join("&"),{endpoint:f,region:O,accessKeyId:B,secretAccessKey:z}=s,R=new URL(f.startsWith("http")?f:`https://${f.replace("://","")}`),d=new Date().toISOString().replace(Q,""),E=d.slice(0,8),C=o&&typeof o.getReader==="function",j=C?"UNSIGNED-PAYLOAD":await H(o||""),u=L(L({host:R.host,"x-amz-date":d,"x-amz-content-sha256":j,...o&&!C?{"content-length":`${h(o).byteLength}`}:{}},r.headers,""),r.meta,"x-amz-meta-"),S=Object.keys(u).map((n)=>n.toLowerCase()).sort(),M=[i,x||"/",y,S.map((n)=>`${n}:${u[n]?.trim()}`).join(`
2
+ `)+`
3
+ `,S.join(";"),j].join(`
4
+ `),k=`${E}/${O}/s3/aws4_request`,W=await g(await g(await g(await g(`${T}${z}`,E),O),"s3"),"aws4_request"),v=K(await g(W,[U,d,k,await H(M)].join(`
5
+ `)));u.Authorization=`${U} Credential=${B}/${k}, SignedHeaders=${S.join(";")}, Signature=${v}`;let _=new URL(x,R);if(y)_.search=y;try{let n=await fetch(_,{method:i,headers:u,body:o});if(!n.ok)throw{status:n.status,...A(await n.text())};if(i==="GET"&&!t.key){let p=new Headers(n.headers),c=A((await n.text()).trim());if(c&&typeof c==="object"){let D=Object.keys(c);if(D.length===1)c=c[D[0]]}return p.set("content-type","application/json"),new Response(JSON.stringify(c),{status:n.status,statusText:n.statusText,headers:p})}return n}catch(n){if(n.status)throw n;throw{status:500,Error:{Code:n?.code||"InternalServerError",Message:n?.message||String(n)}}}},config:s,clone:(t={})=>V({...s,...t})}}export{H as hash,g as hmac,K as toHex,V as zs3};
6
+
7
+ //# debugId=A52263D00FB93F3664756E2164756E21
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../index.ts"],
4
+ "sourcesContent": [
5
+ "\nexport interface S3Config {\n endpoint: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucket?: string;\n region?: string;\n [key: string]: any;\n}\n\nexport interface S3Options {\n headers?: Record<string, string>;\n meta?: Record<string, string>;\n query?: Record<string, string | number | boolean>;\n}\n\n// 1. Самый полный интерфейс (для GET)\nexport interface S3GetOptions {\n key?: string;\n bucket?: string;\n prefix?: string;\n maxKeys?: number;\n}\n\n// 2. Для точечных операций забираем только key и bucket, делая key ОБЯЗАТЕЛЬНЫМ\nexport interface S3ObjectOptions extends Pick<S3GetOptions, 'bucket'> {\n key: string;\n}\n\n// 3. Для PUT добавляем к ним body\nexport interface S3PutOptions extends S3ObjectOptions {\n body?: any;\n}\n\n// 1. Возвращаем строгий Union-тип\nexport type S3RequestOptions =\n | ({ method: 'GET' } & S3GetOptions)\n | ({ method: 'PUT' } & S3PutOptions)\n | ({ method: 'DELETE' } & S3ObjectOptions)\n | ({ method: 'HEAD' } & S3ObjectOptions);\n\n// 2. Упрощаем интерфейс S3Client, убирая перегрузки\nexport interface S3Client {\n /** Execute an S3 request (supports GET, PUT, DELETE, HEAD) */\n execute(req: S3RequestOptions, options?: S3Options): Promise<Response>;\n config: S3Config & { region: string; bucket: string };\n clone(newConfig?: Partial<S3Config>): S3Client;\n}\n\nexport interface S3StatResponse extends Record<string, any> {\n size: number;\n mtime: Date;\n}\n\nconst ALG_NAME = ('HMAC' + '');\nconst ALG_HASH = ('SHA-256' + '');\nconst AWS4 = ('AWS4' + '');\nconst SIGN_P = [AWS4, ALG_NAME, 'SHA256'].join('-');\nconst RE_ISO_DATE = /[:-]|\\.\\d{3}/g;\nconst RE_XML_TAGS = /<(\\w[-\\w]*)(?:\\/|[^>]*>((?:(?!<\\1)[\\s\\S])*)<\\/\\1)>/gm;\nconst RE_DASH_CASE = /\\B[A-Z]/g;\nconst RE_ENCODE_URI = /[!'()*]/g;\nconst RE_LEADING_SLASHES = /^\\/*/;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\nconst { subtle } = crypto;\n\nconst toArrayBuffer = (d: any): ArrayBuffer =>\n d instanceof ArrayBuffer ? d : d?.buffer?.constructor === ArrayBuffer ? d.buffer : encoder.encode(d == null ? '' : `${d}`).buffer;\n\nexport const toHex = (buf: ArrayBuffer | Uint8Array):string =>\n Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');\n\nexport const hmac = async (key: string | Uint8Array | ArrayBuffer, data: string): Promise<Uint8Array> => {\n const cryptoKey = await subtle.importKey('raw', toArrayBuffer(key), { name: ALG_NAME, hash: ALG_HASH}, false, ['sign']);\n return new Uint8Array(await subtle.sign(ALG_NAME, cryptoKey, encoder.encode(data)));\n};\n\nexport const hash = async (data: any) => toHex(await subtle.digest(ALG_HASH, toArrayBuffer(data)));\n\nconst toKebabCase = (str: string): string =>\n str.replace(RE_DASH_CASE, \"-$&\").toLowerCase();\n\nconst assignDashCase = (target: Record<string, string>, source: Record<string, any> | undefined, prefix: string): Record<string, string> => {\n if (source) Object.keys(source).forEach(key => {\n const name = toKebabCase(prefix + key);\n if (source[key] && typeof source[key] !== 'object') target[name] = `${source[key]}`;\n });\n return target;\n};\n\nconst awsEncodeURI = (str: string): string =>\n encodeURIComponent(str).replace(RE_ENCODE_URI, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n\nconst parseXml = (str: string): any => {\n const types: Record<string, any> = { \"\": \"\", \"true\": true, \"false\": false };\n const m = [...str.matchAll(RE_XML_TAGS)];\n return m.length ? m.reduce((acc: any, [, key = '', content = '']) => {\n const value = types[content] ?? (isNaN(+content) ? parseXml(content) : +content);\n acc[key] = Array.isArray(acc[key]) ? [...acc[key], value] : acc[key] != null ? [acc[key], value] : key === \"Contents\" ? [value] : value;\n return acc;\n }, {}) : str;\n};\n\nexport function zs3(opts: S3Config): S3Client {\n const config = { region: 'us-east-1', bucket: '', ...opts };\n const execute = async (req: S3RequestOptions, options: S3Options = {}) => {\n const { method } = req;\n const body = 'body' in req ? req.body : null;\n\n // Build uri\n const targetBucket = req.bucket !== undefined ? req.bucket : config.bucket;\n const cleanKey = 'key' in req && req.key ? req.key.replace(RE_LEADING_SLASHES, '') : '';\n const uri = targetBucket ? `/${targetBucket}/${cleanKey}` : `/${cleanKey}`;\n const isListQuery = method === 'GET' && !('key' in req);\n const sortedQuery = Object.entries({\n ...options.query,\n ...(isListQuery && {\n listType: 2,\n maxKeys: req.maxKeys || 1000,\n prefix: req.prefix || ''\n })\n })\n .sort()\n .map(([key, val]) => `${toKebabCase(key)}=${awsEncodeURI(String(val ?? ''))}`)\n .join('&');\n\n // --- Оригинальный блок конфигурации ---\n const { endpoint, region, accessKeyId, secretAccessKey } = config;\n const base = new URL(endpoint.startsWith('http') ? endpoint : `https://${endpoint.replace('://', '')}`);\n const isoDate = new Date().toISOString().replace(RE_ISO_DATE, '');\n const sDate = isoDate.slice(0, 8);\n\n const isStream = body && typeof body.getReader === 'function';\n const payloadHash = isStream ? 'UNSIGNED-PAYLOAD' : await hash(body || '');\n\n const headers: Record<string, string> = assignDashCase(assignDashCase({\n host: base.host,\n 'x-amz-date': isoDate,\n 'x-amz-content-sha256': payloadHash,\n ...(body && !isStream ? { 'content-length': `${toArrayBuffer(body).byteLength}` } : {})\n }, options.headers, \"\"), options.meta, \"x-amz-meta-\");\n\n const sortedHeaders = Object.keys(headers).map(k => k.toLowerCase()).sort();\n\n // 4. Формируем Canonical Request (используем уже готовые uri и sortedQuery)\n const canonical = [\n method,\n uri || '/',\n sortedQuery,\n sortedHeaders.map(k => `${k}:${headers[k]?.trim()}`).join('\\n') + '\\n',\n sortedHeaders.join(';'),\n payloadHash\n ].join('\\n');\n\n const scope = `${sDate}/${region}/s3/aws4_request`;\n\n const kSign = await hmac(await hmac(await hmac(await hmac(`${AWS4}${secretAccessKey}`, sDate), region), 's3'), 'aws4_request');\n const signature = toHex(await hmac(kSign, [SIGN_P, isoDate, scope, await hash(canonical)].join('\\n')));\n\n headers['Authorization'] = `${SIGN_P} Credential=${accessKeyId}/${scope}, SignedHeaders=${sortedHeaders.join(';')}, Signature=${signature}`;\n\n const requestUrl = new URL(uri, base);\n\n if (sortedQuery) {\n requestUrl.search = sortedQuery;\n }\n\n try {\n const res = await fetch(requestUrl, { method, headers, body });\n\n if (!res.ok) {\n throw { status: res.status, ...parseXml(await res.text()) };\n }\n\n if (method === 'GET' && !req.key) {\n const headers = new Headers(res.headers);\n\n let data = parseXml((await res.text()).trim());\n\n if (data && typeof data === 'object') {\n const keys = Object.keys(data);\n if (keys.length === 1) data = data[keys[0] as string];\n }\n\n headers.set('content-type', 'application/json');\n\n return new Response(JSON.stringify(data), {\n status: res.status,\n statusText: res.statusText,\n headers\n });\n }\n\n return res;\n } catch (err: any) {\n if (err.status) throw err;\n throw { status: 500, Error: { Code: err?.code || 'InternalServerError', Message: err?.message || String(err) } };\n }\n };\n\n return {\n execute,\n config,\n clone: (newConfig: Partial<S3Config> = {}) => zs3({ ...config, ...newConfig })\n };\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "A52263D00FB93F3664756E2164756E21",
9
+ "names": []
10
+ }
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  export interface S3Config {
2
3
  endpoint: string;
3
4
  accessKeyId: string;
@@ -10,13 +11,42 @@ export interface S3Config {
10
11
  export interface S3Options {
11
12
  headers?: Record<string, string>;
12
13
  meta?: Record<string, string>;
14
+ query?: Record<string, string | number | boolean>;
13
15
  }
14
16
 
15
- export interface ListOptions {
17
+ // 1. Самый полный интерфейс (для GET)
18
+ export interface S3GetOptions {
19
+ key?: string;
20
+ bucket?: string;
16
21
  prefix?: string;
17
22
  maxKeys?: number;
18
23
  }
19
24
 
25
+ // 2. Для точечных операций забираем только key и bucket, делая key ОБЯЗАТЕЛЬНЫМ
26
+ export interface S3ObjectOptions extends Pick<S3GetOptions, 'bucket'> {
27
+ key: string;
28
+ }
29
+
30
+ // 3. Для PUT добавляем к ним body
31
+ export interface S3PutOptions extends S3ObjectOptions {
32
+ body?: any;
33
+ }
34
+
35
+ // 1. Возвращаем строгий Union-тип
36
+ export type S3RequestOptions =
37
+ | ({ method: 'GET' } & S3GetOptions)
38
+ | ({ method: 'PUT' } & S3PutOptions)
39
+ | ({ method: 'DELETE' } & S3ObjectOptions)
40
+ | ({ method: 'HEAD' } & S3ObjectOptions);
41
+
42
+ // 2. Упрощаем интерфейс S3Client, убирая перегрузки
43
+ export interface S3Client {
44
+ /** Execute an S3 request (supports GET, PUT, DELETE, HEAD) */
45
+ execute(req: S3RequestOptions, options?: S3Options): Promise<Response>;
46
+ config: S3Config & { region: string; bucket: string };
47
+ clone(newConfig?: Partial<S3Config>): S3Client;
48
+ }
49
+
20
50
  export interface S3StatResponse extends Record<string, any> {
21
51
  size: number;
22
52
  mtime: Date;
@@ -27,30 +57,34 @@ const ALG_HASH = ('SHA-256' + '');
27
57
  const AWS4 = ('AWS4' + '');
28
58
  const SIGN_P = [AWS4, ALG_NAME, 'SHA256'].join('-');
29
59
  const RE_ISO_DATE = /[:-]|\.\d{3}/g;
30
- const RE_XML_TAGS = /<(\w)([-\w]*)(?:\/|[^>]*>((?:(?!<\1\2)[\s\S])*)<\/\1\2)>/gm;
60
+ const RE_XML_TAGS = /<(\w[-\w]*)(?:\/|[^>]*>((?:(?!<\1)[\s\S])*)<\/\1)>/gm;
31
61
  const RE_DASH_CASE = /\B[A-Z]/g;
32
62
  const RE_ENCODE_URI = /[!'()*]/g;
33
63
  const RE_LEADING_SLASHES = /^\/*/;
34
64
 
35
- const enc = new TextEncoder();
65
+ const encoder = new TextEncoder();
66
+ const decoder = new TextDecoder();
36
67
  const { subtle } = crypto;
37
68
 
38
69
  const toArrayBuffer = (d: any): ArrayBuffer =>
39
- d instanceof ArrayBuffer ? d : d?.buffer?.constructor === ArrayBuffer ? d.buffer : enc.encode(d == null ? '' : `${d}`).buffer;
70
+ d instanceof ArrayBuffer ? d : d?.buffer?.constructor === ArrayBuffer ? d.buffer : encoder.encode(d == null ? '' : `${d}`).buffer;
40
71
 
41
72
  export const toHex = (buf: ArrayBuffer | Uint8Array):string =>
42
73
  Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
43
74
 
44
75
  export const hmac = async (key: string | Uint8Array | ArrayBuffer, data: string): Promise<Uint8Array> => {
45
76
  const cryptoKey = await subtle.importKey('raw', toArrayBuffer(key), { name: ALG_NAME, hash: ALG_HASH}, false, ['sign']);
46
- return new Uint8Array(await subtle.sign(ALG_NAME, cryptoKey, enc.encode(data)));
77
+ return new Uint8Array(await subtle.sign(ALG_NAME, cryptoKey, encoder.encode(data)));
47
78
  };
48
79
 
49
80
  export const hash = async (data: any) => toHex(await subtle.digest(ALG_HASH, toArrayBuffer(data)));
50
81
 
82
+ const toKebabCase = (str: string): string =>
83
+ str.replace(RE_DASH_CASE, "-$&").toLowerCase();
84
+
51
85
  const assignDashCase = (target: Record<string, string>, source: Record<string, any> | undefined, prefix: string): Record<string, string> => {
52
86
  if (source) Object.keys(source).forEach(key => {
53
- const name = (prefix + (key === "expires" ? key : key.replace(RE_DASH_CASE, "-$&"))).toLowerCase();
87
+ const name = toKebabCase(prefix + key);
54
88
  if (source[key] && typeof source[key] !== 'object') target[name] = `${source[key]}`;
55
89
  });
56
90
  return target;
@@ -62,42 +96,42 @@ const awsEncodeURI = (str: string): string =>
62
96
  const parseXml = (str: string): any => {
63
97
  const types: Record<string, any> = { "": "", "true": true, "false": false };
64
98
  const m = [...str.matchAll(RE_XML_TAGS)];
65
- return m.length ? m.reduce((acc: any, [, tag = '', suffix = '', content = '']) => {
66
- const k = tag.toLowerCase() + suffix;
67
- const v = types[content] ?? (isNaN(+content) ? parseXml(content) : +content);
68
- acc[k] = Array.isArray(acc[k]) ? [...acc[k], v] : acc[k] != null ? [acc[k], v] : k === "contents" ? [v] : v;
99
+ return m.length ? m.reduce((acc: any, [, key = '', content = '']) => {
100
+ const value = types[content] ?? (isNaN(+content) ? parseXml(content) : +content);
101
+ acc[key] = Array.isArray(acc[key]) ? [...acc[key], value] : acc[key] != null ? [acc[key], value] : key === "Contents" ? [value] : value;
69
102
  return acc;
70
103
  }, {}) : str;
71
104
  };
72
105
 
73
- export class S3XmlTransformStream extends TransformStream<Uint8Array, any> {
74
- constructor() {
75
- const decoder = new TextDecoder();
76
- let acc = "";
77
- super({
78
- transform(chunk) { acc += decoder.decode(chunk, { stream: true }); },
79
- flush(c) {
80
- const res = parseXml((acc + decoder.decode()).trim());
81
- c.enqueue(res?.listBucketResult || res);
82
- }
83
- });
84
- }
85
- }
86
-
87
- export function zs3(opts: S3Config) {
106
+ export function zs3(opts: S3Config): S3Client {
88
107
  const config = { region: 'us-east-1', bucket: '', ...opts };
89
- const execute = async (method: string, path: string, body: any = null, options: S3Options = {}): Promise<Response | any> => {
90
- const {
91
- endpoint,
92
- bucket,
93
- region,
94
- accessKeyId,
95
- secretAccessKey
96
- } = config;
108
+ const execute = async (req: S3RequestOptions, options: S3Options = {}) => {
109
+ const { method } = req;
110
+ const body = 'body' in req ? req.body : null;
111
+
112
+ // Build uri
113
+ const targetBucket = req.bucket !== undefined ? req.bucket : config.bucket;
114
+ const cleanKey = 'key' in req && req.key ? req.key.replace(RE_LEADING_SLASHES, '') : '';
115
+ const uri = targetBucket ? `/${targetBucket}/${cleanKey}` : `/${cleanKey}`;
116
+ const isListQuery = method === 'GET' && !('key' in req);
117
+ const sortedQuery = Object.entries({
118
+ ...options.query,
119
+ ...(isListQuery && {
120
+ listType: 2,
121
+ maxKeys: req.maxKeys || 1000,
122
+ prefix: req.prefix || ''
123
+ })
124
+ })
125
+ .sort()
126
+ .map(([key, val]) => `${toKebabCase(key)}=${awsEncodeURI(String(val ?? ''))}`)
127
+ .join('&');
128
+
129
+ // --- Оригинальный блок конфигурации ---
130
+ const { endpoint, region, accessKeyId, secretAccessKey } = config;
97
131
  const base = new URL(endpoint.startsWith('http') ? endpoint : `https://${endpoint.replace('://', '')}`);
98
132
  const isoDate = new Date().toISOString().replace(RE_ISO_DATE, '');
99
133
  const sDate = isoDate.slice(0, 8);
100
- const pathname = bucket ? `/${bucket}/${path.replace(RE_LEADING_SLASHES, '')}` : path;
134
+
101
135
  const isStream = body && typeof body.getReader === 'function';
102
136
  const payloadHash = isStream ? 'UNSIGNED-PAYLOAD' : await hash(body || '');
103
137
 
@@ -108,11 +142,18 @@ export function zs3(opts: S3Config) {
108
142
  ...(body && !isStream ? { 'content-length': `${toArrayBuffer(body).byteLength}` } : {})
109
143
  }, options.headers, ""), options.meta, "x-amz-meta-");
110
144
 
111
- const [uri, query = ''] = pathname.split('?');
112
145
  const sortedHeaders = Object.keys(headers).map(k => k.toLowerCase()).sort();
113
- const sortedQuery = query ? query.split('&').sort().join('&') : '';
114
146
 
115
- const canonical = [method, uri || '/', sortedQuery, sortedHeaders.map(k => `${k}:${headers[k]?.trim()}`).join('\n') + '\n', sortedHeaders.join(';'), payloadHash].join('\n');
147
+ // 4. Формируем Canonical Request (используем уже готовые uri и sortedQuery)
148
+ const canonical = [
149
+ method,
150
+ uri || '/',
151
+ sortedQuery,
152
+ sortedHeaders.map(k => `${k}:${headers[k]?.trim()}`).join('\n') + '\n',
153
+ sortedHeaders.join(';'),
154
+ payloadHash
155
+ ].join('\n');
156
+
116
157
  const scope = `${sDate}/${region}/s3/aws4_request`;
117
158
 
118
159
  const kSign = await hmac(await hmac(await hmac(await hmac(`${AWS4}${secretAccessKey}`, sDate), region), 's3'), 'aws4_request');
@@ -120,33 +161,48 @@ export function zs3(opts: S3Config) {
120
161
 
121
162
  headers['Authorization'] = `${SIGN_P} Credential=${accessKeyId}/${scope}, SignedHeaders=${sortedHeaders.join(';')}, Signature=${signature}`;
122
163
 
123
- const res = await fetch(new URL(pathname, base), { method, headers, body });
124
- if (!res.ok) throw new Error(`S3 Error: ${res.status} - ${await res.text()}`);
164
+ const requestUrl = new URL(uri, base);
125
165
 
126
- return method === 'HEAD' ? {
127
- size: +res.headers.get("content-length")!,
128
- mtime: new Date(res.headers.get("last-modified")!),
129
- ...Object.fromEntries(res.headers)
130
- } : res;
131
- };
166
+ if (sortedQuery) {
167
+ requestUrl.search = sortedQuery;
168
+ }
132
169
 
133
- return {
134
- get: async (keyOrOpts: string | ListOptions, options: S3Options = {}) => {
135
- if (typeof keyOrOpts !== 'object' || keyOrOpts === null) {
136
- return execute('GET', keyOrOpts, null, options);
170
+ try {
171
+ const res = await fetch(requestUrl, { method, headers, body });
172
+
173
+ if (!res.ok) {
174
+ throw { status: res.status, ...parseXml(await res.text()) };
137
175
  }
138
176
 
139
- const query = `?list-type=2&max-keys=${keyOrOpts.maxKeys || 1000}&prefix=${awsEncodeURI(keyOrOpts.prefix || '')}`;
140
- const res = await execute('GET', query, null, options);
177
+ if (method === 'GET' && !req.key) {
178
+ const headers = new Headers(res.headers);
179
+
180
+ let data = parseXml((await res.text()).trim());
141
181
 
142
- const reader = res.body?.pipeThrough(new S3XmlTransformStream()).getReader();
182
+ if (data && typeof data === 'object') {
183
+ const keys = Object.keys(data);
184
+ if (keys.length === 1) data = data[keys[0] as string];
185
+ }
143
186
 
144
- return (await reader?.read()).value
145
- },
146
- put: (path: string, body: any = null, options: S3Options = {}) => execute('PUT', path, body, options),
147
- del: (path: string, options: S3Options = {}) => execute('DELETE', path, null, options),
148
- stat: (path: string, options: S3Options = {}) => execute('HEAD', path, null, options),
187
+ headers.set('content-type', 'application/json');
188
+
189
+ return new Response(JSON.stringify(data), {
190
+ status: res.status,
191
+ statusText: res.statusText,
192
+ headers
193
+ });
194
+ }
195
+
196
+ return res;
197
+ } catch (err: any) {
198
+ if (err.status) throw err;
199
+ throw { status: 500, Error: { Code: err?.code || 'InternalServerError', Message: err?.message || String(err) } };
200
+ }
201
+ };
202
+
203
+ return {
149
204
  execute,
150
- config
205
+ config,
206
+ clone: (newConfig: Partial<S3Config> = {}) => zs3({ ...config, ...newConfig })
151
207
  };
152
208
  }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@focus-at/zs3",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "Zero-dependency micro S3 client built on Web Standards",
5
5
  "homepage": "https://github.com/focus-at/zs3",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./index.ts",
10
- "import": "./dist/zeros3.js"
10
+ "default": "./dist/index.js"
11
11
  }
12
12
  },
13
13
  "scripts": {
14
- "build": "bun build --minify --sourcemap ./index.ts --outdir=dist && mv dist/index.js dist/zeros3.js && mv dist/index.js.map dist/zeros3.js.map",
14
+ "build": "bun build --minify --sourcemap ./index.ts --outdir=dist",
15
15
  "release": "npm version patch && git push origin main --tags",
16
16
  "postinstall": "node -e \"if(!process.env.DISABLE_OPENCOLLECTIVE && !process.env.CI){process.stdout.write('\\u001b[35mThanks for installing ZeroS3! Consider giving it a star on GitHub: https://github.com\\u001b[0m\\n')}\""
17
17
  },
@@ -58,7 +58,7 @@
58
58
  "LICENSE"
59
59
  ],
60
60
  "peerDependencies": {
61
- "typescript": "^5.0.0 || ^6.0.0"
61
+ "typescript": "^6.0.0"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "typescript": {
package/dist/zeros3.js DELETED
@@ -1,8 +0,0 @@
1
- var y="HMAC",C="SHA-256",H="AWS4",_=[H,y,"SHA256"].join("-"),M=/[:-]|\.\d{3}/g,W=/<(\w)([-\w]*)(?:\/|[^>]*>((?:(?!<\1\2)[\s\S])*)<\/\1\2)>/gm,v=/\B[A-Z]/g,X=/[!'()*]/g,k=/^\/*/,b=new TextEncoder,{subtle:A}=crypto,m=(e)=>e instanceof ArrayBuffer?e:e?.buffer?.constructor===ArrayBuffer?e.buffer:b.encode(e==null?"":`${e}`).buffer,I=(e)=>Array.from(new Uint8Array(e)).map((n)=>n.toString(16).padStart(2,"0")).join(""),u=async(e,n)=>{let r=await A.importKey("raw",m(e),{name:y,hash:C},!1,["sign"]);return new Uint8Array(await A.sign(y,r,b.encode(n)))},U=async(e)=>I(await A.digest(C,m(e))),j=(e,n,r)=>{if(n)Object.keys(n).forEach((t)=>{let s=(r+(t==="expires"?t:t.replace(v,"-$&"))).toLowerCase();if(n[t]&&typeof n[t]!=="object")e[s]=`${n[t]}`});return e},Q=(e)=>encodeURIComponent(e).replace(X,(n)=>`%${n.charCodeAt(0).toString(16).toUpperCase()}`),T=(e)=>{let n={"":"",true:!0,false:!1},r=[...e.matchAll(W)];return r.length?r.reduce((t,[,s="",a="",i=""])=>{let o=s.toLowerCase()+a,c=n[i]??(isNaN(+i)?T(i):+i);return t[o]=Array.isArray(t[o])?[...t[o],c]:t[o]!=null?[t[o],c]:o==="contents"?[c]:c,t},{}):e};class B extends TransformStream{constructor(){let e=new TextDecoder,n="";super({transform(r){n+=e.decode(r,{stream:!0})},flush(r){let t=T((n+e.decode()).trim());r.enqueue(t?.listBucketResult||t)}})}}function Y(e){let n={region:"us-east-1",bucket:"",...e},r=async(t,s,a=null,i={})=>{let{endpoint:o,bucket:c,region:S,accessKeyId:G,secretAccessKey:K}=n,w=new URL(o.startsWith("http")?o:`https://${o.replace("://","")}`),l=new Date().toISOString().replace(M,""),h=l.slice(0,8),x=c?`/${c}/${s.replace(k,"")}`:s,E=a&&typeof a.getReader==="function",R=E?"UNSIGNED-PAYLOAD":await U(a||""),f=j(j({host:w.host,"x-amz-date":l,"x-amz-content-sha256":R,...a&&!E?{"content-length":`${m(a).byteLength}`}:{}},i.headers,""),i.meta,"x-amz-meta-"),[N,D=""]=x.split("?"),d=Object.keys(f).map((p)=>p.toLowerCase()).sort(),z=D?D.split("&").sort().join("&"):"",O=[t,N||"/",z,d.map((p)=>`${p}:${f[p]?.trim()}`).join(`
2
- `)+`
3
- `,d.join(";"),R].join(`
4
- `),L=`${h}/${S}/s3/aws4_request`,q=await u(await u(await u(await u(`${H}${K}`,h),S),"s3"),"aws4_request"),P=I(await u(q,[_,l,L,await U(O)].join(`
5
- `)));f.Authorization=`${_} Credential=${G}/${L}, SignedHeaders=${d.join(";")}, Signature=${P}`;let g=await fetch(new URL(x,w),{method:t,headers:f,body:a});if(!g.ok)throw Error(`S3 Error: ${g.status} - ${await g.text()}`);return t==="HEAD"?{size:+g.headers.get("content-length"),mtime:new Date(g.headers.get("last-modified")),...Object.fromEntries(g.headers)}:g};return{get:async(t,s={})=>{if(typeof t!=="object"||t===null)return r("GET",t,null,s);let a=`?list-type=2&max-keys=${t.maxKeys||1000}&prefix=${Q(t.prefix||"")}`;return(await(await r("GET",a,null,s)).body?.pipeThrough(new B).getReader()?.read()).value},put:(t,s=null,a={})=>r("PUT",t,s,a),del:(t,s={})=>r("DELETE",t,null,s),stat:(t,s={})=>r("HEAD",t,null,s),execute:r,config:n}}export{B as S3XmlTransformStream,U as hash,u as hmac,I as toHex,Y as zs3};
6
-
7
- //# debugId=84835880639AD3D564756E2164756E21
8
- //# sourceMappingURL=index.js.map
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../index.ts"],
4
- "sourcesContent": [
5
- "export interface S3Config {\n endpoint: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucket?: string;\n region?: string;\n [key: string]: any;\n}\n\nexport interface S3Options {\n headers?: Record<string, string>;\n meta?: Record<string, string>;\n}\n\nexport interface ListOptions {\n prefix?: string;\n maxKeys?: number;\n}\n\nexport interface S3StatResponse extends Record<string, any> {\n size: number;\n mtime: Date;\n}\n\nconst ALG_NAME = ('HMAC' + '');\nconst ALG_HASH = ('SHA-256' + '');\nconst AWS4 = ('AWS4' + '');\nconst SIGN_P = [AWS4, ALG_NAME, 'SHA256'].join('-');\nconst RE_ISO_DATE = /[:-]|\\.\\d{3}/g;\nconst RE_XML_TAGS = /<(\\w)([-\\w]*)(?:\\/|[^>]*>((?:(?!<\\1\\2)[\\s\\S])*)<\\/\\1\\2)>/gm;\nconst RE_DASH_CASE = /\\B[A-Z]/g;\nconst RE_ENCODE_URI = /[!'()*]/g;\nconst RE_LEADING_SLASHES = /^\\/*/;\n\nconst enc = new TextEncoder();\nconst { subtle } = crypto;\n\nconst toArrayBuffer = (d: any): ArrayBuffer =>\n d instanceof ArrayBuffer ? d : d?.buffer?.constructor === ArrayBuffer ? d.buffer : enc.encode(d == null ? '' : `${d}`).buffer;\n\nexport const toHex = (buf: ArrayBuffer | Uint8Array):string =>\n Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');\n\nexport const hmac = async (key: string | Uint8Array | ArrayBuffer, data: string): Promise<Uint8Array> => {\n const cryptoKey = await subtle.importKey('raw', toArrayBuffer(key), { name: ALG_NAME, hash: ALG_HASH}, false, ['sign']);\n return new Uint8Array(await subtle.sign(ALG_NAME, cryptoKey, enc.encode(data)));\n};\n\nexport const hash = async (data: any) => toHex(await subtle.digest(ALG_HASH, toArrayBuffer(data)));\n\nconst assignDashCase = (target: Record<string, string>, source: Record<string, any> | undefined, prefix: string): Record<string, string> => {\n if (source) Object.keys(source).forEach(key => {\n const name = (prefix + (key === \"expires\" ? key : key.replace(RE_DASH_CASE, \"-$&\"))).toLowerCase();\n if (source[key] && typeof source[key] !== 'object') target[name] = `${source[key]}`;\n });\n return target;\n};\n\nconst awsEncodeURI = (str: string): string =>\n encodeURIComponent(str).replace(RE_ENCODE_URI, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n\nconst parseXml = (str: string): any => {\n const types: Record<string, any> = { \"\": \"\", \"true\": true, \"false\": false };\n const m = [...str.matchAll(RE_XML_TAGS)];\n return m.length ? m.reduce((acc: any, [, tag = '', suffix = '', content = '']) => {\n const k = tag.toLowerCase() + suffix;\n const v = types[content] ?? (isNaN(+content) ? parseXml(content) : +content);\n acc[k] = Array.isArray(acc[k]) ? [...acc[k], v] : acc[k] != null ? [acc[k], v] : k === \"contents\" ? [v] : v;\n return acc;\n }, {}) : str;\n};\n\nexport class S3XmlTransformStream extends TransformStream<Uint8Array, any> {\n constructor() {\n const decoder = new TextDecoder();\n let acc = \"\";\n super({\n transform(chunk) { acc += decoder.decode(chunk, { stream: true }); },\n flush(c) {\n const res = parseXml((acc + decoder.decode()).trim());\n c.enqueue(res?.listBucketResult || res);\n }\n });\n }\n}\n\nexport function zs3(opts: S3Config) {\n const config = { region: 'us-east-1', bucket: '', ...opts };\n const execute = async (method: string, path: string, body: any = null, options: S3Options = {}): Promise<Response | any> => {\n const {\n endpoint,\n bucket,\n region,\n accessKeyId,\n secretAccessKey\n } = config;\n const base = new URL(endpoint.startsWith('http') ? endpoint : `https://${endpoint.replace('://', '')}`);\n const isoDate = new Date().toISOString().replace(RE_ISO_DATE, '');\n const sDate = isoDate.slice(0, 8);\n const pathname = bucket ? `/${bucket}/${path.replace(RE_LEADING_SLASHES, '')}` : path;\n const isStream = body && typeof body.getReader === 'function';\n const payloadHash = isStream ? 'UNSIGNED-PAYLOAD' : await hash(body || '');\n\n const headers: Record<string, string> = assignDashCase(assignDashCase({\n host: base.host,\n 'x-amz-date': isoDate,\n 'x-amz-content-sha256': payloadHash,\n ...(body && !isStream ? { 'content-length': `${toArrayBuffer(body).byteLength}` } : {})\n }, options.headers, \"\"), options.meta, \"x-amz-meta-\");\n\n const [uri, query = ''] = pathname.split('?');\n const sortedHeaders = Object.keys(headers).map(k => k.toLowerCase()).sort();\n const sortedQuery = query ? query.split('&').sort().join('&') : '';\n\n const canonical = [method, uri || '/', sortedQuery, sortedHeaders.map(k => `${k}:${headers[k]?.trim()}`).join('\\n') + '\\n', sortedHeaders.join(';'), payloadHash].join('\\n');\n const scope = `${sDate}/${region}/s3/aws4_request`;\n\n const kSign = await hmac(await hmac(await hmac(await hmac(`${AWS4}${secretAccessKey}`, sDate), region), 's3'), 'aws4_request');\n const signature = toHex(await hmac(kSign, [SIGN_P, isoDate, scope, await hash(canonical)].join('\\n')));\n\n headers['Authorization'] = `${SIGN_P} Credential=${accessKeyId}/${scope}, SignedHeaders=${sortedHeaders.join(';')}, Signature=${signature}`;\n\n const res = await fetch(new URL(pathname, base), { method, headers, body });\n if (!res.ok) throw new Error(`S3 Error: ${res.status} - ${await res.text()}`);\n\n return method === 'HEAD' ? {\n size: +res.headers.get(\"content-length\")!,\n mtime: new Date(res.headers.get(\"last-modified\")!),\n ...Object.fromEntries(res.headers)\n } : res;\n };\n\n return {\n get: async (keyOrOpts: string | ListOptions, options: S3Options = {}) => {\n if (typeof keyOrOpts !== 'object' || keyOrOpts === null) {\n return execute('GET', keyOrOpts, null, options);\n }\n\n const query = `?list-type=2&max-keys=${keyOrOpts.maxKeys || 1000}&prefix=${awsEncodeURI(keyOrOpts.prefix || '')}`;\n const res = await execute('GET', query, null, options);\n\n const reader = res.body?.pipeThrough(new S3XmlTransformStream()).getReader();\n\n return (await reader?.read()).value\n },\n put: (path: string, body: any = null, options: S3Options = {}) => execute('PUT', path, body, options),\n del: (path: string, options: S3Options = {}) => execute('DELETE', path, null, options),\n stat: (path: string, options: S3Options = {}) => execute('HEAD', path, null, options),\n execute,\n config\n };\n}\n"
6
- ],
7
- "mappings": "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",
8
- "debugId": "84835880639AD3D564756E2164756E21",
9
- "names": []
10
- }