@focus-at/zs3 0.0.13 → 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 +31 -35
- package/dist/index.js +8 -0
- package/dist/index.js.map +10 -0
- package/index.ts +116 -60
- package/package.json +4 -4
- package/dist/zeros3.js +0 -8
- package/dist/zeros3.js.map +0 -10
package/README.md
CHANGED
|
@@ -1,49 +1,45 @@
|
|
|
1
|
-
# ZeroS3
|
|
1
|
+
# ZeroS3 (zs3)
|
|
2
2
|

|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
```
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
//
|
|
31
|
-
const
|
|
32
|
-
const
|
|
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
|
-
//
|
|
35
|
-
const
|
|
36
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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,47 +11,80 @@ 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
|
-
|
|
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;
|
|
23
53
|
}
|
|
24
54
|
|
|
25
55
|
const ALG_NAME = ('HMAC' + '');
|
|
26
|
-
const ALG_HASH = ('
|
|
56
|
+
const ALG_HASH = ('SHA-256' + '');
|
|
27
57
|
const AWS4 = ('AWS4' + '');
|
|
28
|
-
const SIGN_P = [AWS4, ALG_NAME,
|
|
58
|
+
const SIGN_P = [AWS4, ALG_NAME, 'SHA256'].join('-');
|
|
29
59
|
const RE_ISO_DATE = /[:-]|\.\d{3}/g;
|
|
30
|
-
const RE_XML_TAGS = /<(\w
|
|
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
|
|
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 :
|
|
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,
|
|
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 +
|
|
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, [,
|
|
66
|
-
const
|
|
67
|
-
|
|
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
|
|
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 (
|
|
90
|
-
const {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
124
|
-
if (!res.ok) throw new Error(`S3 Error: ${res.status} - ${await res.text()}`);
|
|
164
|
+
const requestUrl = new URL(uri, base);
|
|
125
165
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
...Object.fromEntries(res.headers)
|
|
130
|
-
} : res;
|
|
131
|
-
};
|
|
166
|
+
if (sortedQuery) {
|
|
167
|
+
requestUrl.search = sortedQuery;
|
|
168
|
+
}
|
|
132
169
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
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
|
-
"
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "bun build --minify --sourcemap ./index.ts --outdir=dist
|
|
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": "^
|
|
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",m="SHA256",b="AWS4",U=[b,y,m].join("-"),M=/[:-]|\.\d{3}/g,W=/<(\w)([-\w]*)(?:\/|[^>]*>((?:(?!<\1\2)[\s\S])*)<\/\1\2)>/gm,v=/\B[A-Z]/g,X=/[!'()*]/g,k=/^\/*/,H=new TextEncoder,{subtle:A}=crypto,S=(e)=>e instanceof ArrayBuffer?e:e?.buffer?.constructor===ArrayBuffer?e.buffer:H.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",S(e),{name:y,hash:m},!1,["sign"]);return new Uint8Array(await A.sign(y,r,H.encode(n)))},j=async(e)=>I(await A.digest(m,S(e))),C=(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:w,accessKeyId:G,secretAccessKey:K}=n,h=new URL(o.startsWith("http")?o:`https://${o.replace("://","")}`),l=new Date().toISOString().replace(M,""),x=l.slice(0,8),E=c?`/${c}/${s.replace(k,"")}`:s,R=a&&typeof a.getReader==="function",D=R?"UNSIGNED-PAYLOAD":await j(a||""),f=C(C({host:h.host,"x-amz-date":l,"x-amz-content-sha256":D,...a&&!R?{"content-length":`${S(a).byteLength}`}:{}},i.headers,""),i.meta,"x-amz-meta-"),[N,L=""]=E.split("?"),d=Object.keys(f).map((p)=>p.toLowerCase()).sort(),z=L?L.split("&").sort().join("&"):"",O=[t,N||"/",z,d.map((p)=>`${p}:${f[p]?.trim()}`).join(`
|
|
2
|
-
`)+`
|
|
3
|
-
`,d.join(";"),D].join(`
|
|
4
|
-
`),_=`${x}/${w}/s3/aws4_request`,q=await u(await u(await u(await u(`${b}${K}`,x),w),"s3"),"aws4_request"),P=I(await u(q,[U,l,_,await j(O)].join(`
|
|
5
|
-
`)));f.Authorization=`${U} Credential=${G}/${_}, SignedHeaders=${d.join(";")}, Signature=${P}`;let g=await fetch(new URL(E,h),{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,j as hash,u as hmac,I as toHex,Y as zs3};
|
|
6
|
-
|
|
7
|
-
//# debugId=40B38E1FDF2C5C1664756E2164756E21
|
|
8
|
-
//# sourceMappingURL=index.js.map
|
package/dist/zeros3.js.map
DELETED
|
@@ -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 = ('SHA256' + '');\nconst AWS4 = ('AWS4' + '');\nconst SIGN_P = [AWS4, ALG_NAME, ALG_HASH].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": "40B38E1FDF2C5C1664756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|