@focus-at/zs3 0.0.19 → 0.0.22

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/index.js CHANGED
@@ -1,8 +1,15 @@
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};
1
+ var{subtle:U}=crypto,_="HMAC",Y="SHA-256",Z="AWS4",p="X-Amz-",z="UNSIGNED-PAYLOAD",M="Credential",X="Signature",v="SignedHeaders",I=[Z,_,"SHA256"].join("-"),rt=/[:-]|\.\d{3}/g,ot=/<(\w[-\w]*)(?:\/|[^>]*>((?:(?!<\1)[\s\S])*)<\/\1)>/gm,it=/\B[A-Z]/g,at=/[!'()*]/g,ct=/^\/*/,F=new TextEncoder,H=(t)=>t instanceof ArrayBuffer?t:t?.buffer?.constructor===ArrayBuffer?t.buffer:F.encode(t==null?"":`${t}`).buffer,V=(t)=>Array.from(new Uint8Array(t)).map((e)=>e.toString(16).padStart(2,"0")).join(""),A=async(t,e)=>{let c=await U.importKey("raw",H(t),{name:_,hash:Y},!1,["sign"]);return new Uint8Array(await U.sign(_,c,F.encode(e)))},W=async(t)=>V(await U.digest(Y,H(t))),tt=(t)=>t.replace(it,"-$&").toLowerCase(),J=(t,e,c)=>{if(e)Object.keys(e).forEach((n)=>{let o=tt(c+n);if(e[n]&&typeof e[n]!=="object")t[o]=`${e[n]}`});return t},Q=(t)=>encodeURIComponent(t).replace(at,(e)=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`),G=(t)=>{let e={"":"",true:!0,false:!1},c=[...t.matchAll(ot)];return c.length?c.reduce((n,[,o="",f=""])=>{let g=e[f]??(isNaN(+f)?G(f):+f);return n[o]=Array.isArray(n[o])?[...n[o],g]:n[o]!=null?[n[o],g]:o==="Contents"?[g]:g,n},{}):t};function pt(t){let e={region:"us-east-1",bucket:"",...t},{endpoint:c,region:n,accessKeyId:o,secretAccessKey:f,sessionToken:g}=e,[,et=globalThis.location?.protocol||"https",R]=c?.match(/^(?:([^/:]+)?:\/\/)?([^/:]+)/)||[],T=async(r,u={},i=!1)=>{let{method:O,body:S,maxKeys:s=1000,prefix:E="",bucket:d=e.bucket}=r,b=r.key?.replace(ct,"")??"",l=d?`/${d}/${b}`:`/${b}`,a=new Date().toISOString().replace(rt,""),m=a.slice(0,8),N=`${m}/${n}/s3/aws4_request`,k=`${o}/${N}`,q=S?.getReader,L=i||q?z:await W(S||""),K={...u.query,...(O==="GET"||!r.key)&&{"list-type":2,maxKeys:s,prefix:E}},P=J(J({host:R,[p+"content-sha256"]:L,...S&&!q?{"content-length":`${H(S).byteLength}`}:{}},u.headers,""),u.meta,p+"meta-"),h=i?K:P;if(h[p+"Date"]=a,h[p+"Algorithm"]=I,h[p+M]=k,g)h[p+"Security-Token"]=g;if(i)h[p+v]="host";let x=Object.entries(K).sort().map(([y,D])=>`${i?Q(y):tt(y)}=${Q(String(D??""))}`).join("&"),w=Object.fromEntries(Object.entries(P).map(([y,D])=>[y.toLowerCase(),D])),C=Object.keys(w).sort(),nt=`${O}
2
+ ${l}
3
+ ${x}
4
+ `+(i?`host:${R}
6
5
 
7
- //# debugId=A52263D00FB93F3664756E2164756E21
6
+ host
7
+ ${z}`:`${C.map((y)=>`${y}:${w[y]?.trim()}`).join(`
8
+ `)}
9
+
10
+ ${C.join(";")}
11
+ ${L}`),st=await A(await A(await A(await A(`${Z}${f}`,m),n),"s3"),"aws4_request"),B=V(await A(st,[I,a,N,await W(nt)].join(`
12
+ `))),j=new URL(l,`${et}://${R}`);if(x)j.search=x;if(i)j.searchParams.append(p+X,B);else w.Authorization=`${I} ${M}=${k}, ${v}=${C.join(";")}, ${X}=${B}`;return{requestUrl:j,headers:w,body:S}};return{sign:async(r,u={})=>{let{requestUrl:i}=await T(r,u,!0);return i.toString()},execute:async(r,u={})=>{let{requestUrl:i,headers:O,body:S}=await T(r,u,!1);try{let s=await fetch(i,{method:r.method,headers:O,body:S}),{ok:E,status:d,statusText:b}=s;if(!E)throw{status:d,...G(await s.text())};if(r.method==="GET"&&!r.key){let l=new Headers(s.headers),a=G(await s.text());if(a&&typeof a==="object"){let m=Object.keys(a);if(m.length===1)a=a[m[0]]}return l.set("content-type","application/json"),new Response(JSON.stringify(a),{status:d,statusText:b,headers:l})}return s}catch(s){if(s.status)throw s;throw{status:500,Error:{Code:s?.code||"InternalServerError",Message:s?.message||String(s)}}}},clone:(r={})=>pt({...e,...r})}}export{W as hash,A as hmac,V as toHex,pt as zs3};
13
+
14
+ //# debugId=01228C5122C452DC64756E2164756E21
8
15
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../index.ts"],
4
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"
5
+ "\nexport interface S3Config {\n endpoint: string;\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken: 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\nexport interface S3Client {\n /** Generate a presigned URL for secure, temporary access to an S3 object */\n sign(req: S3RequestOptions, options?: S3Options & { expiresIn?: number }): Promise<string>;\n\n /** Execute an S3 request directly (supports GET, PUT, DELETE, HEAD) */\n execute(req: S3RequestOptions, options?: S3Options): Promise<Response>;\n\n /** Create a new client instance by shallow-merging the current config with overrides */\n clone(newConfig?: Partial<S3Config>): S3Client;\n}\n\nexport interface S3StatResponse extends Record<string, any> {\n size: number;\n mtime: Date;\n}\nconst { subtle } = crypto;\nconst ALG_NAME = ('HMAC' + '');\nconst ALG_HASH = ('SHA-256' + '');\nconst AWS4 = ('AWS4' + '');\nconst X = ('X-Amz-' + '');\nconst UNSIGNED = ('UNSIGNED-PAYLOAD' + '');\nconst CREDENTIAL = ('Credential' + '');\nconst SIGNATURE = ('Signature' + '');\nconst S_HEADERS = ('SignedHeaders' + '');\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();\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\n\nexport function zs3(opts: S3Config): S3Client {\n const config = { region: 'us-east-1', bucket: '', ...opts };\n const { endpoint, region, accessKeyId, secretAccessKey, sessionToken } = config;\n const [,protocol = (globalThis.location?.protocol || 'https'), host] = endpoint?.match(/^(?:([^/:]+)?:\\/\\/)?([^/:]+)/) || [];\n\n\n\n const prepare = async (req: S3RequestOptions, options: S3Options & { expiresIn?: number } = {}, isPresigned = false) => {\n const { method, body, maxKeys = 1000, prefix = '', bucket = config.bucket } = req as S3RequestOptions & (S3PutOptions & S3GetOptions);\n const cleanKey = req.key?.replace(RE_LEADING_SLASHES, '') ?? '';\n const uri = bucket ? `/${bucket}/${cleanKey}` : `/${cleanKey}`;\n const isoDate = new Date().toISOString().replace(RE_ISO_DATE, '');\n const sDate = isoDate.slice(0, 8);\n const scope = `${sDate}/${region}/s3/aws4_request`;\n const credential = `${accessKeyId}/${scope}`;\n const isStream = body?.getReader;\n const payloadHash = isPresigned || isStream ? UNSIGNED : await hash(body || '');\n\n\n const queryObj: Record<string, any> = {\n ...options.query,\n ...((method === 'GET' || !req.key) && { 'list-type': 2, maxKeys, prefix })\n };\n\n const rawHeaders: Record<string, string> = assignDashCase(assignDashCase({\n host,\n [X + 'content-sha256']: payloadHash,\n ...(body && !isStream ? { 'content-length': `${toArrayBuffer(body).byteLength}` } : {})\n }, options.headers, \"\"), options.meta, X + 'meta-');\n\n const target = isPresigned ? queryObj : rawHeaders;\n\n target[X + 'Date'] = isoDate;\n target[X + 'Algorithm'] = SIGN_P;\n target[X + CREDENTIAL] = credential;\n\n if (sessionToken) target[X + 'Security-Token'] = sessionToken;\n if (isPresigned) target[X + S_HEADERS] = 'host';\n\n const sortedQuery = Object.entries(queryObj)\n .sort()\n .map(([k, v]) => `${isPresigned ? awsEncodeURI(k) : toKebabCase(k)}=${awsEncodeURI(String(v ?? ''))}`)\n .join('&');\n\n const headers = Object.fromEntries(Object.entries(rawHeaders).map(([k, v]) => [k.toLowerCase(), v]));\n const sortedHeaders = Object.keys(headers).sort();\n\n const start = `${method}\\n${uri}\\n${sortedQuery}\\n`;\n const canonical = start + (isPresigned\n ? `host:${host}\\n\\nhost\\n${UNSIGNED}`\n : `${sortedHeaders.map(k => `${k}:${headers[k]?.trim()}`).join('\\n')}\\n\\n${sortedHeaders.join(';')}\\n${payloadHash}`\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 const requestUrl = new URL(uri, `${protocol}://${host}`);\n if (sortedQuery) requestUrl.search = sortedQuery;\n\n if (isPresigned) {\n requestUrl.searchParams.append(X + SIGNATURE, signature);\n } else {\n headers['Authorization'] = `${SIGN_P} ${CREDENTIAL}=${credential}, ${S_HEADERS}=${sortedHeaders.join(';')}, ${SIGNATURE}=${signature}`;\n }\n\n return { requestUrl, headers, body };\n };\n\n const sign = async (req: S3RequestOptions, options: S3Options & { expiresIn?: number } = {}): Promise<string> => {\n const { requestUrl } = await prepare(req, options, true);\n return requestUrl.toString();\n };\n\n const execute = async (req: S3RequestOptions, options: S3Options = {}): Promise<Response> => {\n const { requestUrl, headers, body } = await prepare(req, options, false);\n\n try {\n const res = await fetch(requestUrl, { method: req.method, headers, body });\n const { ok, status, statusText } = res;\n\n if (!ok) {\n throw { status, ...parseXml(await res.text()) };\n }\n\n if (req.method === 'GET' && !req.key) {\n const headers = new Headers(res.headers);\n\n let data = parseXml(await res.text());\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,\n 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 sign,\n execute,\n clone: (newConfig: Partial<S3Config> = {}) => zs3({ ...config, ...newConfig })\n };\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "A52263D00FB93F3664756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "01228C5122C452DC64756E2164756E21",
9
9
  "names": []
10
10
  }
package/index.ts CHANGED
@@ -3,6 +3,7 @@ export interface S3Config {
3
3
  endpoint: string;
4
4
  accessKeyId: string;
5
5
  secretAccessKey: string;
6
+ sessionToken: string;
6
7
  bucket?: string;
7
8
  region?: string;
8
9
  [key: string]: any;
@@ -39,11 +40,14 @@ export type S3RequestOptions =
39
40
  | ({ method: 'DELETE' } & S3ObjectOptions)
40
41
  | ({ method: 'HEAD' } & S3ObjectOptions);
41
42
 
42
- // 2. Упрощаем интерфейс S3Client, убирая перегрузки
43
43
  export interface S3Client {
44
- /** Execute an S3 request (supports GET, PUT, DELETE, HEAD) */
44
+ /** Generate a presigned URL for secure, temporary access to an S3 object */
45
+ sign(req: S3RequestOptions, options?: S3Options & { expiresIn?: number }): Promise<string>;
46
+
47
+ /** Execute an S3 request directly (supports GET, PUT, DELETE, HEAD) */
45
48
  execute(req: S3RequestOptions, options?: S3Options): Promise<Response>;
46
- config: S3Config & { region: string; bucket: string };
49
+
50
+ /** Create a new client instance by shallow-merging the current config with overrides */
47
51
  clone(newConfig?: Partial<S3Config>): S3Client;
48
52
  }
49
53
 
@@ -51,10 +55,15 @@ export interface S3StatResponse extends Record<string, any> {
51
55
  size: number;
52
56
  mtime: Date;
53
57
  }
54
-
58
+ const { subtle } = crypto;
55
59
  const ALG_NAME = ('HMAC' + '');
56
60
  const ALG_HASH = ('SHA-256' + '');
57
61
  const AWS4 = ('AWS4' + '');
62
+ const X = ('X-Amz-' + '');
63
+ const UNSIGNED = ('UNSIGNED-PAYLOAD' + '');
64
+ const CREDENTIAL = ('Credential' + '');
65
+ const SIGNATURE = ('Signature' + '');
66
+ const S_HEADERS = ('SignedHeaders' + '');
58
67
  const SIGN_P = [AWS4, ALG_NAME, 'SHA256'].join('-');
59
68
  const RE_ISO_DATE = /[:-]|\.\d{3}/g;
60
69
  const RE_XML_TAGS = /<(\w[-\w]*)(?:\/|[^>]*>((?:(?!<\1)[\s\S])*)<\/\1)>/gm;
@@ -63,8 +72,6 @@ const RE_ENCODE_URI = /[!'()*]/g;
63
72
  const RE_LEADING_SLASHES = /^\/*/;
64
73
 
65
74
  const encoder = new TextEncoder();
66
- const decoder = new TextDecoder();
67
- const { subtle } = crypto;
68
75
 
69
76
  const toArrayBuffer = (d: any): ArrayBuffer =>
70
77
  d instanceof ArrayBuffer ? d : d?.buffer?.constructor === ArrayBuffer ? d.buffer : encoder.encode(d == null ? '' : `${d}`).buffer;
@@ -103,81 +110,94 @@ const parseXml = (str: string): any => {
103
110
  }, {}) : str;
104
111
  };
105
112
 
113
+
106
114
  export function zs3(opts: S3Config): S3Client {
107
115
  const config = { region: 'us-east-1', bucket: '', ...opts };
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('&');
116
+ const { endpoint, region, accessKeyId, secretAccessKey, sessionToken } = config;
117
+ const [,protocol = (globalThis.location?.protocol || 'https'), host] = endpoint?.match(/^(?:([^/:]+)?:\/\/)?([^/:]+)/) || [];
118
+
128
119
 
129
- // --- Оригинальный блок конфигурации ---
130
- const { endpoint, region, accessKeyId, secretAccessKey } = config;
131
- const base = new URL(endpoint.startsWith('http') ? endpoint : `https://${endpoint.replace('://', '')}`);
120
+
121
+ const prepare = async (req: S3RequestOptions, options: S3Options & { expiresIn?: number } = {}, isPresigned = false) => {
122
+ const { method, body, maxKeys = 1000, prefix = '', bucket = config.bucket } = req as S3RequestOptions & (S3PutOptions & S3GetOptions);
123
+ const cleanKey = req.key?.replace(RE_LEADING_SLASHES, '') ?? '';
124
+ const uri = bucket ? `/${bucket}/${cleanKey}` : `/${cleanKey}`;
132
125
  const isoDate = new Date().toISOString().replace(RE_ISO_DATE, '');
133
126
  const sDate = isoDate.slice(0, 8);
127
+ const scope = `${sDate}/${region}/s3/aws4_request`;
128
+ const credential = `${accessKeyId}/${scope}`;
129
+ const isStream = body?.getReader;
130
+ const payloadHash = isPresigned || isStream ? UNSIGNED : await hash(body || '');
134
131
 
135
- const isStream = body && typeof body.getReader === 'function';
136
- const payloadHash = isStream ? 'UNSIGNED-PAYLOAD' : await hash(body || '');
137
132
 
138
- const headers: Record<string, string> = assignDashCase(assignDashCase({
139
- host: base.host,
140
- 'x-amz-date': isoDate,
141
- 'x-amz-content-sha256': payloadHash,
133
+ const queryObj: Record<string, any> = {
134
+ ...options.query,
135
+ ...((method === 'GET' || !req.key) && { 'list-type': 2, maxKeys, prefix })
136
+ };
137
+
138
+ const rawHeaders: Record<string, string> = assignDashCase(assignDashCase({
139
+ host,
140
+ [X + 'content-sha256']: payloadHash,
142
141
  ...(body && !isStream ? { 'content-length': `${toArrayBuffer(body).byteLength}` } : {})
143
- }, options.headers, ""), options.meta, "x-amz-meta-");
142
+ }, options.headers, ""), options.meta, X + 'meta-');
144
143
 
145
- const sortedHeaders = Object.keys(headers).map(k => k.toLowerCase()).sort();
144
+ const target = isPresigned ? queryObj : rawHeaders;
146
145
 
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');
146
+ target[X + 'Date'] = isoDate;
147
+ target[X + 'Algorithm'] = SIGN_P;
148
+ target[X + CREDENTIAL] = credential;
156
149
 
157
- const scope = `${sDate}/${region}/s3/aws4_request`;
150
+ if (sessionToken) target[X + 'Security-Token'] = sessionToken;
151
+ if (isPresigned) target[X + S_HEADERS] = 'host';
152
+
153
+ const sortedQuery = Object.entries(queryObj)
154
+ .sort()
155
+ .map(([k, v]) => `${isPresigned ? awsEncodeURI(k) : toKebabCase(k)}=${awsEncodeURI(String(v ?? ''))}`)
156
+ .join('&');
157
+
158
+ const headers = Object.fromEntries(Object.entries(rawHeaders).map(([k, v]) => [k.toLowerCase(), v]));
159
+ const sortedHeaders = Object.keys(headers).sort();
158
160
 
161
+ const start = `${method}\n${uri}\n${sortedQuery}\n`;
162
+ const canonical = start + (isPresigned
163
+ ? `host:${host}\n\nhost\n${UNSIGNED}`
164
+ : `${sortedHeaders.map(k => `${k}:${headers[k]?.trim()}`).join('\n')}\n\n${sortedHeaders.join(';')}\n${payloadHash}`
165
+ );
159
166
  const kSign = await hmac(await hmac(await hmac(await hmac(`${AWS4}${secretAccessKey}`, sDate), region), 's3'), 'aws4_request');
160
167
  const signature = toHex(await hmac(kSign, [SIGN_P, isoDate, scope, await hash(canonical)].join('\n')));
161
168
 
162
- headers['Authorization'] = `${SIGN_P} Credential=${accessKeyId}/${scope}, SignedHeaders=${sortedHeaders.join(';')}, Signature=${signature}`;
169
+ const requestUrl = new URL(uri, `${protocol}://${host}`);
170
+ if (sortedQuery) requestUrl.search = sortedQuery;
163
171
 
164
- const requestUrl = new URL(uri, base);
165
-
166
- if (sortedQuery) {
167
- requestUrl.search = sortedQuery;
172
+ if (isPresigned) {
173
+ requestUrl.searchParams.append(X + SIGNATURE, signature);
174
+ } else {
175
+ headers['Authorization'] = `${SIGN_P} ${CREDENTIAL}=${credential}, ${S_HEADERS}=${sortedHeaders.join(';')}, ${SIGNATURE}=${signature}`;
168
176
  }
169
177
 
178
+ return { requestUrl, headers, body };
179
+ };
180
+
181
+ const sign = async (req: S3RequestOptions, options: S3Options & { expiresIn?: number } = {}): Promise<string> => {
182
+ const { requestUrl } = await prepare(req, options, true);
183
+ return requestUrl.toString();
184
+ };
185
+
186
+ const execute = async (req: S3RequestOptions, options: S3Options = {}): Promise<Response> => {
187
+ const { requestUrl, headers, body } = await prepare(req, options, false);
188
+
170
189
  try {
171
- const res = await fetch(requestUrl, { method, headers, body });
190
+ const res = await fetch(requestUrl, { method: req.method, headers, body });
191
+ const { ok, status, statusText } = res;
172
192
 
173
- if (!res.ok) {
174
- throw { status: res.status, ...parseXml(await res.text()) };
193
+ if (!ok) {
194
+ throw { status, ...parseXml(await res.text()) };
175
195
  }
176
196
 
177
- if (method === 'GET' && !req.key) {
197
+ if (req.method === 'GET' && !req.key) {
178
198
  const headers = new Headers(res.headers);
179
199
 
180
- let data = parseXml((await res.text()).trim());
200
+ let data = parseXml(await res.text());
181
201
 
182
202
  if (data && typeof data === 'object') {
183
203
  const keys = Object.keys(data);
@@ -187,8 +207,8 @@ export function zs3(opts: S3Config): S3Client {
187
207
  headers.set('content-type', 'application/json');
188
208
 
189
209
  return new Response(JSON.stringify(data), {
190
- status: res.status,
191
- statusText: res.statusText,
210
+ status,
211
+ statusText,
192
212
  headers
193
213
  });
194
214
  }
@@ -201,8 +221,8 @@ export function zs3(opts: S3Config): S3Client {
201
221
  };
202
222
 
203
223
  return {
224
+ sign,
204
225
  execute,
205
- config,
206
226
  clone: (newConfig: Partial<S3Config> = {}) => zs3({ ...config, ...newConfig })
207
227
  };
208
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@focus-at/zs3",
3
- "version": "0.0.19",
3
+ "version": "0.0.22",
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",