@focus-at/zs3 0.0.4 → 0.0.7

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/dist/zeros3.js ADDED
@@ -0,0 +1,5 @@
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};
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@focus-at/zs3",
3
- "version": "0.0.4",
3
+ "version": "0.0.7",
4
4
  "description": "Zero-dependency micro S3 client built on Web Standards",
5
5
  "homepage": "https://github.com/focus-at/zs3#readme",
6
- "module": "index.ts",
7
6
  "main": "./dist/zeros3.js",
8
7
  "types": "./index.ts",
9
8
  "bin": {
@@ -12,10 +11,8 @@
12
11
  "exports": {
13
12
  ".": {
14
13
  "types": "./index.ts",
15
- "import": {
16
- "typescript": "./index.ts",
17
- "default": "./dist/zeros3.js"
18
- }
14
+ "import": "./dist/zeros3.js",
15
+ "default": "./dist/zeros3.js"
19
16
  }
20
17
  },
21
18
  "scripts": {
@@ -52,10 +49,13 @@
52
49
  "url": "https://github.com/focus-at/zs3/issues"
53
50
  }
54
51
  ],
52
+ "files": [
53
+ "dist",
54
+ "index.ts",
55
+ "README.md",
56
+ "LICENSE"
57
+ ],
55
58
  "type": "module",
56
- "devDependencies": {
57
- "@types/bun": "latest"
58
- },
59
59
  "peerDependencies": {
60
60
  "typescript": "^5"
61
61
  }
@@ -1,28 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- jobs:
8
- publish:
9
- runs-on: ubuntu-latest
10
- steps:
11
- - name: Checkout repository
12
- uses: actions/checkout@v4
13
-
14
- - name: Setup Bun
15
- uses: oven-sh/setup-bun@v1
16
- with:
17
- bun-version: latest
18
-
19
- - name: Install dependencies
20
- run: bun install --frozen-lockfile
21
-
22
- - name: Build Library and CLI
23
- run: bun run build
24
-
25
- - name: Publish Package
26
- run: bun publish --access=public
27
- env:
28
- NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,141 +0,0 @@
1
- import { describe, test, expect, afterEach, beforeEach } from "bun:test";
2
- import { zs3, hash, hmac, toHex } from "../index.ts";
3
-
4
- describe("ZeroS3 / zs3 Core Tests", () => {
5
- const originalFetch = globalThis.fetch;
6
-
7
- // Объявляем переменную для хранения инстанса
8
- let storage: ReturnType<typeof zs3>;
9
- let lastRequest = { url: null as URL | null, headers: {} as Record<string, string> };
10
-
11
- const mockFetch = (xmlContent = "", status = 200) => {
12
- globalThis.fetch = (async (url: URL, init?: RequestInit) => {
13
- lastRequest.url = url;
14
- lastRequest.headers = (init?.headers as Record<string, string>) || {};
15
- return new Response(xmlContent, { status });
16
- }) as unknown as typeof fetch;
17
- };
18
-
19
- // ИСПРАВЛЕНО: Сбрасываем и пересоздаем инстанс перед КАЖДЫМ тестом
20
- beforeEach(() => {
21
- storage = zs3({
22
- endpoint: "127.0.0.1:9000",
23
- accessKeyId: "mock-access-key",
24
- secretAccessKey: "mock-secret-key",
25
- bucket: "test-bucket",
26
- });
27
- });
28
-
29
- // Хук afterEach гарантирует, что после каждого теста fetch вернется в исходное состояние
30
- afterEach(() => {
31
- globalThis.fetch = originalFetch;
32
- });
33
-
34
- test("should correctly export core cryptographic utilities with strict check", async () => {
35
- const signature = await hmac("secret", "data");
36
- expect(signature).toBeInstanceOf(Uint8Array);
37
- expect(toHex(signature)).toBe("1b2c16b75bd2a870c114153ccda5bcfca63314bc722fa160d690de133ccbb9db");
38
- expect(await hash("hello zeros3")).toBe("dea9a435609aadbdaf641ddb2b0d1ad1d97263798769b6e43188d0ba57b79fad");
39
- });
40
-
41
- test("should correctly merge config defaults and reactively update on changes", async () => {
42
- expect(storage.config.region).toBe("us-east-1");
43
- expect(storage.config.bucket).toBe("test-bucket");
44
-
45
- mockFetch();
46
-
47
- // 1. Проверяем генерацию путей со старым конфигом
48
- await storage.stat("file.txt");
49
- expect(lastRequest.url?.pathname).toBe("/test-bucket/file.txt");
50
- expect(lastRequest.url?.host).toBe("127.0.0.1:9000");
51
- expect(lastRequest.headers["Authorization"]).toContain("/us-east-1/s3/aws4_request");
52
-
53
- // 2. Меняем конфиг, подсовывая кривой эндпоинт с ://
54
- storage.config.bucket = "dynamic-bucket";
55
- storage.config.endpoint = "://zone.com";
56
- storage.config.region = "eu-west-1";
57
-
58
- // 3. Вызываем деструктурированный put — он обязан отработать реактивно
59
- await storage.put("image.png", "payload");
60
-
61
- expect(lastRequest.url?.host).toBe("zone.com");
62
- expect(lastRequest.url?.protocol).toBe("https:");
63
- expect(lastRequest.url?.pathname).toBe("/dynamic-bucket/image.png");
64
- expect(lastRequest.headers["Authorization"]).toContain("/eu-west-1/s3/aws4_request");
65
- });
66
-
67
- test("should perfectly stream and parse large XML data and empty tags", async () => {
68
- const xmlMock = `
69
- <ListBucketResult>
70
- <Name>test-bucket</Name>
71
- <Prefix>production/</Prefix>
72
- <MaxKeys>1000</MaxKeys>
73
- <IsTruncated>false</IsTruncated>
74
- <Marker></Marker>
75
- <NextMarker/>
76
- <Contents>
77
- <Key>production/app.tar</Key>
78
- <Size>31457280</Size>
79
- </Contents>
80
- <Contents>
81
- <Key>production/config.json</Key>
82
- <Size>2048</Size>
83
- </Contents>
84
- </ListBucketResult>
85
- `;
86
-
87
- globalThis.fetch = (async () => new Response(xmlMock, { status: 200 })) as unknown as typeof fetch;
88
-
89
- const result = await storage.get({ prefix: "production/" });
90
-
91
- // Top Level
92
- expect(result.name).toBe("test-bucket");
93
- expect(result.marker).toBe("");
94
- expect(result.nextMarker).toBe("");
95
- expect(result.isTruncated).toBe(false);
96
- expect(result.maxKeys).toBe(1000);
97
-
98
- // Array
99
- expect(result.contents).toBeInstanceOf(Array);
100
- expect(result.contents.length).toBe(2);
101
- expect(result.contents[0].key).toBe("production/app.tar");
102
- expect(result.contents[0].size).toBe(31457280);
103
- expect(result.contents[1].key).toBe("production/config.json");
104
- expect(result.contents[1].size).toBe(2048);
105
- });
106
-
107
- test("should perfectly stream and parse large XML data, empty tags and clean root prefix", async () => {
108
- mockFetch(`
109
- <ListBucketResult>
110
- <Name>test-bucket</Name>
111
- <Prefix></Prefix>
112
- <MaxKeys>1000</MaxKeys>
113
- <IsTruncated>false</IsTruncated>
114
- <Marker></Marker>
115
- <NextMarker/>
116
- <Contents>
117
- <Key>production/app.tar</Key>
118
- <Size>31457280</Size>
119
- </Contents>
120
- </ListBucketResult>
121
- `)
122
-
123
- // Вызываем с ПУСТЫМ префиксом
124
- const result = await storage.get({ prefix: "" });
125
-
126
- // 1. ПРОВЕРКА ПРЕФИКСА В URL (убеждаемся, что пустой префикс не сломал строку)
127
- expect(lastRequest.url?.search).toContain("prefix=");
128
- expect(lastRequest.url?.pathname).toBe("/test-bucket/"); // Без двойных слэшей на конце!
129
-
130
- // 2. ПРОВЕРКА ПУСТЫХ ТЕГОВ (убеждаемся, что они не стали числами 0 или null)
131
- expect(result.prefix).toBe("");
132
- expect(result.marker).toBe("");
133
- expect(result.nextMarker).toBe("");
134
-
135
- // 3. ПРОВЕРКА ТИПОВ И СТРУКТУРЫ ДАННЫХ
136
- expect(result.isTruncated).toBe(false);
137
- expect(result.maxKeys).toBe(1000);
138
- expect(result.contents[0].key).toBe("production/app.tar");
139
- expect(result.contents[0].size).toBe(31457280);
140
- });
141
- });
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- // Environment setup & latest features
4
- "lib": ["ESNext"],
5
- "target": "ESNext",
6
- "module": "Preserve",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
-
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
16
-
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
21
- "noUncheckedIndexedAccess": true,
22
- "noImplicitOverride": true,
23
-
24
- // Some stricter flags (disabled by default)
25
- "noUnusedLocals": false,
26
- "noUnusedParameters": false,
27
- "noPropertyAccessFromIndexSignature": false
28
- }
29
- }