@focus-at/zs3 0.0.17 → 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/README.md +2 -0
- package/dist/index.js +13 -6
- package/dist/index.js.map +3 -3
- package/index.ts +80 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
> ⚠️ **Status: WIP (Work in Progress)**
|
|
6
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.
|
|
7
|
+
>
|
|
8
|
+
[cdn.jsdelivr](https://cdn.jsdelivr.net/npm/@focus-at/zs3@latest/+esm)
|
|
7
9
|
|
|
8
10
|
## Overview
|
|
9
11
|
**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.
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
var
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
8
|
-
"debugId": "
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
131
|
-
const
|
|
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
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
|
|
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,
|
|
142
|
+
}, options.headers, ""), options.meta, X + 'meta-');
|
|
144
143
|
|
|
145
|
-
const
|
|
144
|
+
const target = isPresigned ? queryObj : rawHeaders;
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
const requestUrl = new URL(uri, `${protocol}://${host}`);
|
|
170
|
+
if (sortedQuery) requestUrl.search = sortedQuery;
|
|
163
171
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 (!
|
|
174
|
-
throw { status
|
|
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(
|
|
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
|
|
191
|
-
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
|
}
|