fast-s3-presigner 1.0.0
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 +137 -0
- package/README.zh-CN.md +137 -0
- package/dist/index.cjs +17 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +101 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# fast-s3-presigner
|
|
2
|
+
|
|
3
|
+
A blazing-fast, zero-dependency S3 presigned URL generator for Node.js.
|
|
4
|
+
|
|
5
|
+
[English] | [简体中文](./README.zh-CN.md)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Blazing Performance** - Optimized specifically for high-throughput and high-concurrency environments.
|
|
10
|
+
- **Zero dependencies** - only uses Node.js built-in `crypto`
|
|
11
|
+
- **Tiny bundle** - ~7KB
|
|
12
|
+
- **Synchronous API** - no async overhead
|
|
13
|
+
- **Dual ESM/CJS** - works everywhere
|
|
14
|
+
- **TypeScript** - full type safety
|
|
15
|
+
- **S3 POST Uploads** - support for browser-based form uploads
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
Node.js >= 20.12.0 (for `crypto.hash()` one-shot API)
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install fast-s3-presigner
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Performance
|
|
28
|
+
|
|
29
|
+
Benchmarked on AMD Ryzen 7 5700G, Node.js v24.14.1 (Raw Operation Latency):
|
|
30
|
+
|
|
31
|
+
| Library | GET (Simple Key) | GET (Complex Key) | PUT | 1000 Sequential GETs |
|
|
32
|
+
|---------|------------------|-------------------|-----|---------------------|
|
|
33
|
+
| **fast-s3-presigner** | **3.58 µs** | **4.84 µs** | **3.43 µs** | **3.46 ms** |
|
|
34
|
+
| lean-s3 | 9.67 µs (2.7x) | 10.24 µs (2.1x) | 8.99 µs (2.6x) | 9.11 ms (2.6x) |
|
|
35
|
+
| aws4 | 15.63 µs (4.4x) | 17.48 µs (3.6x) | 13.93 µs (4.1x) | 14.06 ms (4.1x) |
|
|
36
|
+
| s3mini | 83.97 µs (23.5x) | 86.60 µs (17.9x) | 81.29 µs (23.7x) | 83.50 ms (24.1x) |
|
|
37
|
+
| AWS SDK (v3) | 248.53 µs (69.5x) | 246.20 µs (50.9x) | 254.51 µs (74.3x) | 211.99 ms (61.2x) |
|
|
38
|
+
| aws4fetch | 268.74 µs (75.2x) | 269.95 µs (55.8x) | 250.14 µs (73.0x) | 241.17 ms (69.7x) |
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { S3Presigner } from "fast-s3-presigner";
|
|
44
|
+
|
|
45
|
+
const presigner = new S3Presigner({
|
|
46
|
+
endpoint: "https://my-bucket.s3.us-east-1.amazonaws.com",
|
|
47
|
+
region: "us-east-1",
|
|
48
|
+
bucket: "my-bucket",
|
|
49
|
+
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
|
|
50
|
+
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Generate a download URL (GET, expires in 1 hour)
|
|
54
|
+
const downloadUrl = presigner.presign("photos/image.jpg");
|
|
55
|
+
|
|
56
|
+
// Generate an upload URL (PUT, with content type)
|
|
57
|
+
const uploadUrl = presigner.presign("uploads/file.txt", {
|
|
58
|
+
method: "PUT",
|
|
59
|
+
contentType: "text/plain",
|
|
60
|
+
expiresIn: 3600,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Generate a POST upload policy for browser form submissions
|
|
64
|
+
const postOptions = presigner.presignPost("uploads/video.mp4", {
|
|
65
|
+
expiresIn: 3600,
|
|
66
|
+
conditions: [
|
|
67
|
+
["content-length-range", 0, 10485760] // up to 10MB
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
// <form action={postOptions.url} method="POST" enctype="multipart/form-data">
|
|
71
|
+
// Insert hidden fields from postOptions.fields
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `new S3Presigner(config)`
|
|
77
|
+
|
|
78
|
+
| Option | Type | Required | Description |
|
|
79
|
+
|--------|------|----------|-------------|
|
|
80
|
+
| `endpoint` | `string` | Yes | Full S3 endpoint URL (e.g., `https://my-bucket.s3.us-east-1.amazonaws.com`) |
|
|
81
|
+
| `region` | `string` | Yes | AWS region |
|
|
82
|
+
| `bucket` | `string` | Yes | Bucket name |
|
|
83
|
+
| `accessKeyId` | `string` | Yes | AWS access key ID |
|
|
84
|
+
| `secretAccessKey` | `string` | Yes | AWS secret access key |
|
|
85
|
+
| `sessionToken` | `string` | No | Temporary credentials session token |
|
|
86
|
+
| `forcePathStyle` | `boolean` | No | Force path-style URLs (auto-detected for localhost/IP) |
|
|
87
|
+
|
|
88
|
+
### `presigner.presign(key, options?)`
|
|
89
|
+
|
|
90
|
+
Returns a presigned URL string for GET/PUT/HEAD/DELETE requests.
|
|
91
|
+
|
|
92
|
+
| Option | Type | Default | Description |
|
|
93
|
+
|--------|------|---------|-------------|
|
|
94
|
+
| `method` | `"GET" \| "PUT" \| "DELETE" \| "HEAD"` | `"GET"` | HTTP method |
|
|
95
|
+
| `expiresIn` | `number` | `3600` | Expiration in seconds (1 - 604800) |
|
|
96
|
+
| `contentType` | `string` | - | Content-Type header to sign |
|
|
97
|
+
| `contentLength` | `number` | - | Content-Length header to sign |
|
|
98
|
+
| `acl` | `string` | - | S3 ACL (e.g., `"public-read"`) |
|
|
99
|
+
| `storageClass` | `string` | - | Storage class (e.g., `"GLACIER"`) |
|
|
100
|
+
| `responseContentDisposition` | `string` | - | Response Content-Disposition |
|
|
101
|
+
| `responseContentType` | `string` | - | Response Content-Type |
|
|
102
|
+
| `additionalSignedHeaders` | `Record<string, string>` | - | Extra headers to sign |
|
|
103
|
+
|
|
104
|
+
### `presigner.presignPost(key, options?)`
|
|
105
|
+
|
|
106
|
+
Returns a `PresignPostResult` containing the `url` and `fields` needed for a browser-based HTML form POST upload.
|
|
107
|
+
|
|
108
|
+
| Option | Type | Default | Description |
|
|
109
|
+
|--------|------|---------|-------------|
|
|
110
|
+
| `expiresIn` | `number` | `3600` | Expiration in seconds (1 - 604800) |
|
|
111
|
+
| `conditions` | `Array` | - | Additional POST policy conditions |
|
|
112
|
+
| `fields` | `Record<string, string>` | - | Additional form fields to strictly sign |
|
|
113
|
+
|
|
114
|
+
## Endpoint Examples
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// AWS S3 (virtual-hosted style)
|
|
118
|
+
endpoint: "https://my-bucket.s3.us-east-1.amazonaws.com"
|
|
119
|
+
|
|
120
|
+
// AWS S3 (path style)
|
|
121
|
+
endpoint: "https://s3.us-east-1.amazonaws.com"
|
|
122
|
+
// + forcePathStyle: true
|
|
123
|
+
|
|
124
|
+
// MinIO / LocalStack
|
|
125
|
+
endpoint: "http://localhost:9000"
|
|
126
|
+
// path-style auto-detected for localhost/IP
|
|
127
|
+
|
|
128
|
+
// Cloudflare R2
|
|
129
|
+
endpoint: "https://my-bucket.<account-id>.r2.cloudflarestorage.com"
|
|
130
|
+
|
|
131
|
+
// Custom S3-compatible
|
|
132
|
+
endpoint: "https://s3.example.com"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
ISC
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# fast-s3-presigner
|
|
2
|
+
|
|
3
|
+
极致性能、零依赖、用于 Node.js 的 S3 预签名 URL 生成器。
|
|
4
|
+
|
|
5
|
+
[English](./README.md) | [简体中文]
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- **极致性能** - 专为高吞吐量和高并发环境优化。
|
|
10
|
+
- **零依赖** - 仅使用 Node.js 内置的 `crypto` 模块。
|
|
11
|
+
- **极小体积** - 约 7KB。
|
|
12
|
+
- **同步 API** - 无异步开销。
|
|
13
|
+
- **双模支持 (ESM/CJS)** - 随处可用。
|
|
14
|
+
- **TypeScript** - 完整的类型安全支持。
|
|
15
|
+
- **S3 POST 上传** - 支持浏览器端表单上传。
|
|
16
|
+
|
|
17
|
+
## 要求
|
|
18
|
+
|
|
19
|
+
Node.js >= 20.12.0 (由于使用了 `crypto.hash()` 一次性哈希接口)
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install fast-s3-presigner
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 性能测试
|
|
28
|
+
|
|
29
|
+
测试环境:AMD Ryzen 7 5700G, Node.js v24.14.1 (原始操作延迟):
|
|
30
|
+
|
|
31
|
+
| 库 | GET (简单 Key) | GET (复杂 Key) | PUT | 1000 次顺序 GET |
|
|
32
|
+
|---------|------------------|-------------------|-----|---------------------|
|
|
33
|
+
| **fast-s3-presigner** | **3.58 µs** | **4.84 µs** | **3.43 µs** | **3.46 ms** |
|
|
34
|
+
| lean-s3 | 9.67 µs (2.7x) | 10.24 µs (2.1x) | 8.99 µs (2.6x) | 9.11 ms (2.6x) |
|
|
35
|
+
| aws4 | 15.63 µs (4.4x) | 17.48 µs (3.6x) | 13.93 µs (4.1x) | 14.06 ms (4.1x) |
|
|
36
|
+
| s3mini | 83.97 µs (23.5x) | 86.60 µs (17.9x) | 81.29 µs (23.7x) | 83.50 ms (24.1x) |
|
|
37
|
+
| AWS SDK (v3) | 248.53 µs (69.5x) | 246.20 µs (50.9x) | 254.51 µs (74.3x) | 211.99 ms (61.2x) |
|
|
38
|
+
| aws4fetch | 268.74 µs (75.2x) | 269.95 µs (55.8x) | 250.14 µs (73.0x) | 241.17 ms (69.7x) |
|
|
39
|
+
|
|
40
|
+
## 快速开始
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { S3Presigner } from "fast-s3-presigner";
|
|
44
|
+
|
|
45
|
+
const presigner = new S3Presigner({
|
|
46
|
+
endpoint: "https://my-bucket.s3.us-east-1.amazonaws.com",
|
|
47
|
+
region: "us-east-1",
|
|
48
|
+
bucket: "my-bucket",
|
|
49
|
+
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
|
|
50
|
+
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 生成下载 URL (GET, 1 小时过期)
|
|
54
|
+
const downloadUrl = presigner.presign("photos/image.jpg");
|
|
55
|
+
|
|
56
|
+
// 生成上传 URL (PUT, 带 Content-Type)
|
|
57
|
+
const uploadUrl = presigner.presign("uploads/file.txt", {
|
|
58
|
+
method: "PUT",
|
|
59
|
+
contentType: "text/plain",
|
|
60
|
+
expiresIn: 3600,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 为浏览器表单提交生成 POST 上传策略
|
|
64
|
+
const postOptions = presigner.presignPost("uploads/video.mp4", {
|
|
65
|
+
expiresIn: 3600,
|
|
66
|
+
conditions: [
|
|
67
|
+
["content-length-range", 0, 10485760] // 最大 10MB
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
// <form action={postOptions.url} method="POST" enctype="multipart/form-data">
|
|
71
|
+
// 插入来自 postOptions.fields 的隐藏表单项
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `new S3Presigner(config)`
|
|
77
|
+
|
|
78
|
+
| 选项 | 类型 | 必填 | 描述 |
|
|
79
|
+
|--------|------|----------|-------------|
|
|
80
|
+
| `endpoint` | `string` | 是 | 完整的 S3 端点 URL (例如 `https://my-bucket.s3.us-east-1.amazonaws.com`) |
|
|
81
|
+
| `region` | `string` | 是 | AWS 区域 |
|
|
82
|
+
| `bucket` | `string` | 是 | 存储桶名称 |
|
|
83
|
+
| `accessKeyId` | `string` | 是 | AWS Access Key ID |
|
|
84
|
+
| `secretAccessKey` | `string` | 是 | AWS Secret Access Key |
|
|
85
|
+
| `sessionToken` | `string` | 否 | 临时凭证的 Session Token |
|
|
86
|
+
| `forcePathStyle` | `boolean` | 否 | 强制使用路径风格 URL (localhost/IP 会自动检测) |
|
|
87
|
+
|
|
88
|
+
### `presigner.presign(key, options?)`
|
|
89
|
+
|
|
90
|
+
为 GET/PUT/HEAD/DELETE 请求返回预签名 URL 字符串。
|
|
91
|
+
|
|
92
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
93
|
+
|--------|------|---------|-------------|
|
|
94
|
+
| `method` | `"GET" \| "PUT" \| "DELETE" \| "HEAD"` | `"GET"` | HTTP 方法 |
|
|
95
|
+
| `expiresIn` | `number` | `3600` | 过期时间(秒,范围 1 - 604800) |
|
|
96
|
+
| `contentType` | `string` | - | 需要签名的 Content-Type 标头 |
|
|
97
|
+
| `contentLength` | `number` | - | 需要签名的 Content-Length 标头 |
|
|
98
|
+
| `acl` | `string` | - | S3 ACL (例如 `"public-read"`) |
|
|
99
|
+
| `storageClass` | `string` | - | 存储类型 (例如 `"GLACIER"`) |
|
|
100
|
+
| `responseContentDisposition` | `string` | - | 响应 Content-Disposition 覆盖 |
|
|
101
|
+
| `responseContentType` | `string` | - | 响应 Content-Type 覆盖 |
|
|
102
|
+
| `additionalSignedHeaders` | `Record<string, string>` | - | 其他需要签名的标头 |
|
|
103
|
+
|
|
104
|
+
### `presigner.presignPost(key, options?)`
|
|
105
|
+
|
|
106
|
+
返回一个 `PresignPostResult`,包含浏览器表单 POST 上传所需的 `url` 和 `fields`。
|
|
107
|
+
|
|
108
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
109
|
+
|--------|------|---------|-------------|
|
|
110
|
+
| `expiresIn` | `number` | `3600` | 过期时间(秒,范围 1 - 604800) |
|
|
111
|
+
| `conditions` | `Array` | - | 额外的 POST 策略条件 |
|
|
112
|
+
| `fields` | `Record<string, string>` | - | 额外的表单字段(严格签名) |
|
|
113
|
+
|
|
114
|
+
## 端点示例
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// AWS S3 (虚拟托管风格)
|
|
118
|
+
endpoint: "https://my-bucket.s3.us-east-1.amazonaws.com"
|
|
119
|
+
|
|
120
|
+
// AWS S3 (路径风格)
|
|
121
|
+
endpoint: "https://s3.us-east-1.amazonaws.com"
|
|
122
|
+
// + forcePathStyle: true
|
|
123
|
+
|
|
124
|
+
// MinIO / LocalStack
|
|
125
|
+
endpoint: "http://localhost:9000"
|
|
126
|
+
// localhost/IP 会自动检测为路径风格
|
|
127
|
+
|
|
128
|
+
// Cloudflare R2
|
|
129
|
+
endpoint: "https://my-bucket.<account-id>.r2.cloudflarestorage.com"
|
|
130
|
+
|
|
131
|
+
// 自定义 S3 兼容端点
|
|
132
|
+
endpoint: "https://s3.example.com"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 许可证
|
|
136
|
+
|
|
137
|
+
ISC
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";var k=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var W=(s,e)=>{for(var t in e)k(s,t,{get:e[t],enumerable:!0})},V=(s,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of F(e))!B.call(s,n)&&n!==t&&k(s,n,{get:()=>e[n],enumerable:!(i=L(e,n))||i.enumerable});return s};var j=s=>V(k({},"__esModule",{value:!0}),s);var se={};W(se,{S3Error:()=>a,S3ErrorCode:()=>d,S3Presigner:()=>D});module.exports=j(se);var K=require("crypto");var Y=/^[A-Za-z0-9\-._~/]+$/,Z=/^[A-Za-z0-9\-._~]*$/,J=/[!'()*]/g,Q={"!":"%21","'":"%27","(":"%28",")":"%29","*":"%2A"};function A(s){return encodeURIComponent(s).replace(J,e=>Q[e])}function v(s){if(s.charCodeAt(0)===47&&(s=s.slice(1)),Y.test(s))return s;let e=s.split("/");for(let t=0;t<e.length;t++)Z.test(e[t])||(e[t]=A(e[t]));return e.join("/")}var p=[];for(let s=0;s<100;s++)p[s]=s<10?`0${s}`:`${s}`;function ee(s){let e=new Date(s),t=e.getUTCFullYear(),i=e.getUTCMonth()+1,n=e.getUTCDate(),r=e.getUTCHours(),h=e.getUTCMinutes(),u=e.getUTCSeconds(),o=`${t}${p[i]}${p[n]}`,g=`${o}T${p[r]}${p[h]}${p[u]}Z`;return{date:o,dateTime:g,timestamp:s}}var G=0,N=null;function $(){let s=Math.floor(Date.now()/1e3);if(s===G&&N!==null)return N;let e=ee(s*1e3);return G=s,N=e,N}var y=require("crypto");function te(s,e,t){let i=(0,y.createHmac)("sha256",`AWS4${s}`).update(e).digest(),n=(0,y.createHmac)("sha256",i).update(t).digest(),r=(0,y.createHmac)("sha256",n).update("s3").digest();return(0,y.createHmac)("sha256",r).update("aws4_request").digest()}function b(s,e,t,i){let n=`${s}
|
|
2
|
+
${e}
|
|
3
|
+
${t}
|
|
4
|
+
host:${i}
|
|
5
|
+
|
|
6
|
+
host
|
|
7
|
+
UNSIGNED-PAYLOAD`;return(0,y.hash)("sha256",n,"hex")}function H(s,e,t,i,n,r){let h="";for(let o=0;o<n.length;o++){let g=n[o];h+=`${g}:${i[g]}
|
|
8
|
+
`}let u=`${s}
|
|
9
|
+
${e}
|
|
10
|
+
${t}
|
|
11
|
+
${h}
|
|
12
|
+
${r}
|
|
13
|
+
UNSIGNED-PAYLOAD`;return(0,y.hash)("sha256",u,"hex")}var E=class{constructor(e,t){this.region=e;this.secretAccessKey=t}cachedKey=null;cachedDate="";get(e){return e===this.cachedDate&&this.cachedKey?this.cachedKey:(this.cachedDate=e,this.cachedKey=te(this.secretAccessKey,e,this.region),this.cachedKey)}};var d={MISSING_CONFIG:1e3,INVALID_BUCKET:1001,MISSING_KEY:1002,INVALID_EXPIRATION:1003,INVALID_CONTENT_LENGTH:1004},a=class s extends Error{constructor(t,i){super(t);this.code=i;this.name="S3Error",Error.captureStackTrace&&Error.captureStackTrace(this,s)}};var D=class{region;accessKeyId;bucket;signingKeyCache;urlPrefix;endpointPath;pathPrefix;host;scope;credentialScopeEncoded;credentialPrefix;sessionToken;constructor(e){if(!e.endpoint?.trim())throw new a("endpoint is required",d.MISSING_CONFIG);if(!e.region?.trim())throw new a("region is required",d.MISSING_CONFIG);if(!e.bucket?.trim())throw new a("bucket is required",d.MISSING_CONFIG);if(!e.accessKeyId?.trim())throw new a("accessKeyId is required",d.MISSING_CONFIG);if(!e.secretAccessKey?.trim())throw new a("secretAccessKey is required",d.MISSING_CONFIG);this.validateBucketName(e.bucket),this.region=e.region,this.accessKeyId=e.accessKeyId,this.bucket=e.bucket,this.signingKeyCache=new E(e.region,e.secretAccessKey),this.sessionToken=e.sessionToken||"";let t=e.endpoint,i=t.startsWith("https")?"https://":"http://";t.startsWith("https://")?t=t.slice(8):t.startsWith("http://")&&(t=t.slice(7)),t.endsWith("/")&&(t=t.slice(0,-1));let n=t.indexOf("/"),r,h;n!==-1?(r=t.slice(0,n),h=t.slice(n)):(r=t,h="");let u=r.startsWith("localhost"),o=/^\d+\.\d+\.\d+\.\d+(?::\d+)?$/.test(r),g=e.forcePathStyle??(u||o);g||(r=e.bucket+"."+r),this.host=r,this.endpointPath=h,this.urlPrefix=i+r,this.pathPrefix=g?e.bucket+"/":"",this.credentialPrefix=A(e.accessKeyId)+"%2F",this.credentialScopeEncoded="%2F"+this.region+"%2Fs3%2Faws4_request",this.scope=e.region+"/s3/aws4_request"}validateBucketName(e){if(e.length<3||e.length>63||!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(e))throw new a("Bucket name must be 3-63 characters, lowercase letters, digits, hyphens, and periods",d.INVALID_BUCKET);if(e.includes("..")||e.includes(".-")||e.includes("-."))throw new a("Invalid bucket name: consecutive periods or period-dash",d.INVALID_BUCKET);if(/^\d+\.\d+\.\d+\.\d+$/.test(e))throw new a("Bucket name must not be formatted as an IP address",d.INVALID_BUCKET)}presign(e,t){if(!e)throw new a("key is required",d.MISSING_KEY);let i=t?.method??"GET",n=t?.expiresIn??3600;if(!Number.isFinite(n)||n<1||n>604800||n!==Math.floor(n))throw new a("expiresIn must be an integer between 1 and 604800 seconds",d.INVALID_EXPIRATION);let r=$(),h=this.endpointPath+"/"+this.pathPrefix+v(e),{contentType:u,contentLength:o,additionalSignedHeaders:g,acl:I,responseContentDisposition:P,responseContentType:T,storageClass:c}=t??{},w=!!(u||o!==void 0||g),f,x,z;if(w){if(f=Object.create(null),f.host=this.host,u&&(f["content-type"]=u),o!==void 0){if(!Number.isSafeInteger(o)||o<0)throw new a("contentLength must be a non-negative integer",d.INVALID_CONTENT_LENGTH);f["content-length"]=String(o)}if(g)for(let[l,m]of Object.entries(g)){let S=l.toLowerCase();if(S==="host"||S==="content-type"||S==="content-length")continue;let _=m.trim().replace(/\s+/g," ");f[S]!==void 0?f[S]+=","+_:f[S]=_}z=Object.keys(f).sort(),x=z.join(";")}else x="host";let O=this.credentialPrefix+r.date+this.credentialScopeEncoded,M=!!(I||P||T||c||this.sessionToken),C;if(!M)C="X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential="+O+"&X-Amz-Date="+r.dateTime+"&X-Amz-Expires="+n+"&X-Amz-SignedHeaders="+x;else{let l=[["X-Amz-Algorithm","AWS4-HMAC-SHA256"],["X-Amz-Content-Sha256","UNSIGNED-PAYLOAD"],["X-Amz-Credential",O],["X-Amz-Date",r.dateTime],["X-Amz-Expires",String(n)],["X-Amz-SignedHeaders",x]];I&&l.push(["X-Amz-Acl",A(I)]),P&&l.push(["response-content-disposition",A(P)]),T&&l.push(["response-content-type",A(T)]),c&&l.push(["X-Amz-Storage-Class",A(c)]),this.sessionToken&&l.push(["X-Amz-Security-Token",A(this.sessionToken)]),l.sort((m,S)=>m[0]<S[0]?-1:m[0]>S[0]?1:0),C=l[0][0]+"="+l[0][1];for(let m=1;m<l.length;m++)C+="&"+l[m][0]+"="+l[m][1]}let R=f?H(i,h,C,f,z,x):b(i,h,C,this.host),X=`AWS4-HMAC-SHA256
|
|
14
|
+
`+r.dateTime+`
|
|
15
|
+
`+r.date+"/"+this.scope+`
|
|
16
|
+
`+R,q=this.signingKeyCache.get(r.date),U=(0,K.createHmac)("sha256",q).update(X).digest("hex");return this.urlPrefix+h+"?"+C+"&X-Amz-Signature="+U}presignPost(e,t){if(!e)throw new a("key is required",d.MISSING_KEY);let i=t?.expiresIn??3600;if(!Number.isFinite(i)||i<1||i>604800||i!==Math.floor(i))throw new a("expiresIn must be an integer between 1 and 604800 seconds",d.INVALID_EXPIRATION);let n=$(),r=new Date(n.timestamp+i*1e3),h=r.getUTCFullYear()+"-"+p[r.getUTCMonth()+1]+"-"+p[r.getUTCDate()]+"T"+p[r.getUTCHours()]+":"+p[r.getUTCMinutes()]+":"+p[r.getUTCSeconds()]+"Z",u=this.accessKeyId+"/"+n.date+"/"+this.scope,o=[{bucket:this.bucket},["eq","$key",e],["eq","$x-amz-algorithm","AWS4-HMAC-SHA256"],["eq","$x-amz-credential",u],["eq","$x-amz-date",n.dateTime]];if(this.sessionToken&&o.push(["eq","$x-amz-security-token",this.sessionToken]),t?.acl&&o.push(["eq","$acl",t.acl]),t?.contentType&&o.push(["eq","$content-type",t.contentType]),t?.storageClass&&o.push(["eq","$x-amz-storage-class",t.storageClass]),t?.conditions)for(let w of t.conditions)o.push(w);let g={expiration:h,conditions:o},I=Buffer.from(JSON.stringify(g),"utf8").toString("base64"),P=this.signingKeyCache.get(n.date),T=(0,K.createHmac)("sha256",P).update(I).digest("hex"),c={};return t?.fields&&Object.assign(c,t.fields),c["X-Amz-Algorithm"]="AWS4-HMAC-SHA256",c["X-Amz-Credential"]=u,c["X-Amz-Date"]=n.dateTime,c.key=e,c.Policy=I,c["X-Amz-Signature"]=T,this.sessionToken&&(c["X-Amz-Security-Token"]=this.sessionToken),t?.acl&&(c.acl=t.acl),t?.contentType&&(c["content-type"]=t.contentType),t?.storageClass&&(c["x-amz-storage-class"]=t.storageClass),{url:this.urlPrefix+this.endpointPath+(this.pathPrefix?"/"+this.bucket:""),fields:c}}};0&&(module.exports={S3Error,S3ErrorCode,S3Presigner});
|
|
17
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/presigner.ts","../src/encode.ts","../src/date.ts","../src/signing.ts","../src/error.ts"],"sourcesContent":["export { S3Presigner } from \"./presigner.js\";\r\nexport { S3Error, S3ErrorCode } from \"./error.js\";\r\nexport type {\r\n S3PresignerConfig,\r\n PresignOptions,\r\n PresignPostOptions,\r\n PresignPostResult,\r\n PresignMethod,\r\n} from \"./types.js\";\r\n","import { createHmac } from \"node:crypto\";\r\nimport type { S3PresignerConfig, PresignOptions, PresignPostOptions, PresignPostResult } from \"./types.js\";\r\nimport { encodeS3Path, encodeURIComponentS3 } from \"./encode.js\";\r\nimport { now, PAD } from \"./date.js\";\r\nimport { hashCanonicalRequest, hashCanonicalRequestGeneric, SigningKeyCache } from \"./signing.js\";\r\nimport { S3Error, S3ErrorCode } from \"./error.js\";\r\n\r\nexport class S3Presigner {\r\n private readonly region: string;\r\n private readonly accessKeyId: string;\r\n private readonly bucket: string;\r\n private readonly signingKeyCache: SigningKeyCache;\r\n\r\n private readonly urlPrefix: string;\r\n private readonly endpointPath: string;\r\n private readonly pathPrefix: string;\r\n private readonly host: string;\r\n private readonly scope: string;\r\n private readonly credentialScopeEncoded: string;\r\n private readonly credentialPrefix: string;\r\n private readonly sessionToken: string;\r\n\r\n constructor(config: S3PresignerConfig) {\r\n if (!config.endpoint?.trim()) throw new S3Error(\"endpoint is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.region?.trim()) throw new S3Error(\"region is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.bucket?.trim()) throw new S3Error(\"bucket is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.accessKeyId?.trim()) throw new S3Error(\"accessKeyId is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.secretAccessKey?.trim()) throw new S3Error(\"secretAccessKey is required\", S3ErrorCode.MISSING_CONFIG);\r\n\r\n this.validateBucketName(config.bucket);\r\n\r\n this.region = config.region;\r\n this.accessKeyId = config.accessKeyId;\r\n this.bucket = config.bucket;\r\n this.signingKeyCache = new SigningKeyCache(config.region, config.secretAccessKey);\r\n this.sessionToken = config.sessionToken || \"\";\r\n\r\n let endpoint = config.endpoint;\r\n const protocol = endpoint.startsWith(\"https\") ? \"https://\" : \"http://\";\r\n if (endpoint.startsWith(\"https://\")) endpoint = endpoint.slice(8);\r\n else if (endpoint.startsWith(\"http://\")) endpoint = endpoint.slice(7);\r\n if (endpoint.endsWith(\"/\")) endpoint = endpoint.slice(0, -1);\r\n\r\n // Separate host (domain+port) from any path component\r\n const slashIdx = endpoint.indexOf(\"/\");\r\n let hostPart: string;\r\n let endpointPath: string;\r\n if (slashIdx !== -1) {\r\n hostPart = endpoint.slice(0, slashIdx);\r\n endpointPath = endpoint.slice(slashIdx); // includes leading /\r\n } else {\r\n hostPart = endpoint;\r\n endpointPath = \"\";\r\n }\r\n\r\n const isLocalhost = hostPart.startsWith(\"localhost\");\r\n const isIP = /^\\d+\\.\\d+\\.\\d+\\.\\d+(?::\\d+)?$/.test(hostPart);\r\n const usePathStyle = config.forcePathStyle ?? (isLocalhost || isIP);\r\n\r\n if (!usePathStyle) {\r\n hostPart = config.bucket + \".\" + hostPart;\r\n }\r\n\r\n this.host = hostPart;\r\n this.endpointPath = endpointPath;\r\n this.urlPrefix = protocol + hostPart;\r\n this.pathPrefix = usePathStyle ? config.bucket + \"/\" : \"\";\r\n\r\n this.credentialPrefix = encodeURIComponentS3(config.accessKeyId) + \"%2F\";\r\n this.credentialScopeEncoded = \"%2F\" + this.region + \"%2Fs3%2Faws4_request\";\r\n this.scope = config.region + \"/s3/aws4_request\";\r\n }\r\n\r\n private validateBucketName(bucket: string): void {\r\n if (bucket.length < 3 || bucket.length > 63 || !/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(bucket)) {\r\n throw new S3Error(\"Bucket name must be 3-63 characters, lowercase letters, digits, hyphens, and periods\", S3ErrorCode.INVALID_BUCKET);\r\n }\r\n if (bucket.includes(\"..\") || bucket.includes(\".-\") || bucket.includes(\"-.\")) {\r\n throw new S3Error(\"Invalid bucket name: consecutive periods or period-dash\", S3ErrorCode.INVALID_BUCKET);\r\n }\r\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(bucket)) {\r\n throw new S3Error(\"Bucket name must not be formatted as an IP address\", S3ErrorCode.INVALID_BUCKET);\r\n }\r\n }\r\n\r\n presign(key: string, options?: PresignOptions): string {\r\n if (!key) throw new S3Error(\"key is required\", S3ErrorCode.MISSING_KEY);\r\n\r\n const method = options?.method ?? \"GET\";\r\n const expiresIn = options?.expiresIn ?? 3600;\r\n\r\n if (!Number.isFinite(expiresIn) || expiresIn < 1 || expiresIn > 604800 || expiresIn !== Math.floor(expiresIn)) {\r\n throw new S3Error(\"expiresIn must be an integer between 1 and 604800 seconds\", S3ErrorCode.INVALID_EXPIRATION);\r\n }\r\n\r\n const date = now();\r\n const encodedPath = this.endpointPath + \"/\" + this.pathPrefix + encodeS3Path(key);\r\n\r\n const { \r\n contentType, \r\n contentLength, \r\n additionalSignedHeaders, \r\n acl, \r\n responseContentDisposition, \r\n responseContentType, \r\n storageClass \r\n } = options ?? {};\r\n\r\n // Determine signed headers\r\n const hasExtra = !!(contentType || contentLength !== undefined || additionalSignedHeaders);\r\n let headers: Record<string, string> | undefined;\r\n let signedHeaders: string;\r\n let sortedHeaderKeys: string[] | undefined;\r\n\r\n if (hasExtra) {\r\n headers = Object.create(null) as Record<string, string>;\r\n headers[\"host\"] = this.host;\r\n // Do NOT sign x-amz-date in headers - it's already in the query string\r\n if (contentType) headers[\"content-type\"] = contentType;\r\n if (contentLength !== undefined) {\r\n if (!Number.isSafeInteger(contentLength) || contentLength < 0) throw new S3Error(\"contentLength must be a non-negative integer\", S3ErrorCode.INVALID_CONTENT_LENGTH);\r\n headers[\"content-length\"] = String(contentLength);\r\n }\r\n if (additionalSignedHeaders) {\r\n for (const [k, v] of Object.entries(additionalSignedHeaders)) {\r\n const lowerK = k.toLowerCase();\r\n if (lowerK === \"host\") continue; // host is always set by the library\r\n if (lowerK === \"content-type\" || lowerK === \"content-length\") continue; // managed by dedicated options\r\n const normalizedV = v.trim().replace(/\\s+/g, \" \");\r\n if (headers[lowerK] !== undefined) {\r\n headers[lowerK] += \",\" + normalizedV;\r\n } else {\r\n headers[lowerK] = normalizedV;\r\n }\r\n }\r\n }\r\n sortedHeaderKeys = Object.keys(headers).sort();\r\n signedHeaders = sortedHeaderKeys.join(\";\");\r\n } else {\r\n signedHeaders = \"host\";\r\n }\r\n\r\n // Build query string — fast path for the common case (no optional params), sort only when needed\r\n // The 6 fixed params are already in alphabetical order: Algorithm < Content-Sha256 < Credential < Date < Expires < SignedHeaders\r\n const credentialPart = this.credentialPrefix + date.date + this.credentialScopeEncoded;\r\n const hasOptional = !!(acl || responseContentDisposition || responseContentType || storageClass || this.sessionToken);\r\n\r\n let query: string;\r\n if (!hasOptional) {\r\n // Fast path: direct concatenation, no allocation overhead\r\n query = \"X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=\" + credentialPart\r\n + \"&X-Amz-Date=\" + date.dateTime\r\n + \"&X-Amz-Expires=\" + expiresIn\r\n + \"&X-Amz-SignedHeaders=\" + signedHeaders;\r\n } else {\r\n // Slow path: optional params displace positions, must sort to satisfy SigV4\r\n const params: [string, string][] = [\r\n [\"X-Amz-Algorithm\", \"AWS4-HMAC-SHA256\"],\r\n [\"X-Amz-Content-Sha256\", \"UNSIGNED-PAYLOAD\"],\r\n [\"X-Amz-Credential\", credentialPart],\r\n [\"X-Amz-Date\", date.dateTime],\r\n [\"X-Amz-Expires\", String(expiresIn)],\r\n [\"X-Amz-SignedHeaders\", signedHeaders],\r\n ];\r\n if (acl) params.push([\"X-Amz-Acl\", encodeURIComponentS3(acl)]);\r\n if (responseContentDisposition) params.push([\"response-content-disposition\", encodeURIComponentS3(responseContentDisposition)]);\r\n if (responseContentType) params.push([\"response-content-type\", encodeURIComponentS3(responseContentType)]);\r\n if (storageClass) params.push([\"X-Amz-Storage-Class\", encodeURIComponentS3(storageClass)]);\r\n if (this.sessionToken) params.push([\"X-Amz-Security-Token\", encodeURIComponentS3(this.sessionToken)]);\r\n params.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));\r\n query = params[0][0] + \"=\" + params[0][1];\r\n for (let i = 1; i < params.length; i++) {\r\n query += \"&\" + params[i][0] + \"=\" + params[i][1];\r\n }\r\n }\r\n\r\n // Hash canonical request — use shared signing module (no inline duplication)\r\n const canonicalHash = headers\r\n ? hashCanonicalRequestGeneric(method, encodedPath, query, headers, sortedHeaderKeys!, signedHeaders)\r\n : hashCanonicalRequest(method, encodedPath, query, this.host);\r\n\r\n // Sign\r\n const stringToSign = \"AWS4-HMAC-SHA256\\n\" + date.dateTime + \"\\n\" + date.date + \"/\" + this.scope + \"\\n\" + canonicalHash;\r\n const signingKey = this.signingKeyCache.get(date.date);\r\n const signature = createHmac(\"sha256\", signingKey)\r\n .update(stringToSign)\r\n .digest(\"hex\");\r\n\r\n return this.urlPrefix + encodedPath + \"?\" + query + \"&X-Amz-Signature=\" + signature;\r\n }\r\n\r\n presignPost(key: string, options?: PresignPostOptions): PresignPostResult {\r\n if (!key) throw new S3Error(\"key is required\", S3ErrorCode.MISSING_KEY);\r\n\r\n const expiresIn = options?.expiresIn ?? 3600;\r\n if (!Number.isFinite(expiresIn) || expiresIn < 1 || expiresIn > 604800 || expiresIn !== Math.floor(expiresIn)) {\r\n throw new S3Error(\"expiresIn must be an integer between 1 and 604800 seconds\", S3ErrorCode.INVALID_EXPIRATION);\r\n }\r\n\r\n const date = now();\r\n // Build expiration timestamp without toISOString().replace() chain\r\n const expDate = new Date(date.timestamp + expiresIn * 1000);\r\n const expiration =\r\n expDate.getUTCFullYear() +\r\n \"-\" + PAD[expDate.getUTCMonth() + 1] +\r\n \"-\" + PAD[expDate.getUTCDate()] +\r\n \"T\" + PAD[expDate.getUTCHours()] +\r\n \":\" + PAD[expDate.getUTCMinutes()] +\r\n \":\" + PAD[expDate.getUTCSeconds()] + \"Z\";\r\n const credential = this.accessKeyId + \"/\" + date.date + \"/\" + this.scope;\r\n\r\n const conditions: (string[] | number[] | Record<string, string>)[] = [\r\n { bucket: this.bucket }, // fix: use stored bucket name, not derived from pathPrefix/host\r\n [\"eq\", \"$key\", key],\r\n [\"eq\", \"$x-amz-algorithm\", \"AWS4-HMAC-SHA256\"],\r\n [\"eq\", \"$x-amz-credential\", credential],\r\n [\"eq\", \"$x-amz-date\", date.dateTime],\r\n ];\r\n\r\n if (this.sessionToken) {\r\n conditions.push([\"eq\", \"$x-amz-security-token\", this.sessionToken]);\r\n }\r\n\r\n if (options?.acl) conditions.push([\"eq\", \"$acl\", options.acl]);\r\n if (options?.contentType) conditions.push([\"eq\", \"$content-type\", options.contentType]);\r\n if (options?.storageClass) conditions.push([\"eq\", \"$x-amz-storage-class\", options.storageClass]);\r\n if (options?.conditions) {\r\n for (const cond of options.conditions) {\r\n conditions.push(cond as string[] | number[] | Record<string, string>);\r\n }\r\n }\r\n\r\n const policy = { expiration, conditions };\r\n const encodedPolicy = Buffer.from(JSON.stringify(policy), \"utf8\").toString(\"base64\");\r\n\r\n const signingKey = this.signingKeyCache.get(date.date);\r\n const signature = createHmac(\"sha256\", signingKey).update(encodedPolicy).digest(\"hex\");\r\n\r\n const fields: Record<string, string> = {};\r\n // Merge user-provided fields FIRST so that signing fields always take precedence.\r\n // This prevents callers from accidentally overwriting Policy, X-Amz-Signature, etc.\r\n if (options?.fields) Object.assign(fields, options.fields);\r\n\r\n fields[\"X-Amz-Algorithm\"] = \"AWS4-HMAC-SHA256\";\r\n fields[\"X-Amz-Credential\"] = credential;\r\n fields[\"X-Amz-Date\"] = date.dateTime;\r\n fields.key = key;\r\n fields.Policy = encodedPolicy;\r\n fields[\"X-Amz-Signature\"] = signature;\r\n\r\n if (this.sessionToken) fields[\"X-Amz-Security-Token\"] = this.sessionToken;\r\n if (options?.acl) fields.acl = options.acl;\r\n if (options?.contentType) fields[\"content-type\"] = options.contentType;\r\n if (options?.storageClass) fields[\"x-amz-storage-class\"] = options.storageClass;\r\n\r\n return {\r\n // fix: virtual-hosted style no longer appends a spurious trailing slash\r\n url: this.urlPrefix + this.endpointPath + (this.pathPrefix ? \"/\" + this.bucket : \"\"),\r\n fields,\r\n };\r\n }\r\n}\r\n","// Fast regex for checking if an entire path needs encoding (safe chars + slashes)\r\nconst SAFE_PATH_RE = /^[A-Za-z0-9\\-._~/]+$/;\r\nconst SAFE_SEGMENT_RE = /^[A-Za-z0-9\\-._~]*$/;\r\n\r\n// Single-pass encoding map for S3-specific characters not covered by encodeURIComponent\r\nconst ENCODE_RE = /[!'()*]/g;\r\nconst ENCODE_MAP: Record<string, string> = {\r\n \"!\": \"%21\",\r\n \"'\": \"%27\",\r\n \"(\": \"%28\",\r\n \")\": \"%29\",\r\n \"*\": \"%2A\",\r\n};\r\n\r\n/**\r\n * Encode a URI component with S3-specific requirements.\r\n * Encodes: ! ' ( ) * in addition to standard encodeURIComponent.\r\n * Uses a single .replace() pass instead of five chained calls.\r\n */\r\nexport function encodeURIComponentS3(value: string): string {\r\n return encodeURIComponent(value).replace(ENCODE_RE, c => ENCODE_MAP[c]);\r\n}\r\n\r\n/**\r\n * Encode S3 object key path.\r\n * Preserves forward slashes, encodes segments as needed.\r\n */\r\nexport function encodeS3Path(path: string): string {\r\n // Strip leading slash\r\n if (path.charCodeAt(0) === 47) path = path.slice(1); // 47 = '/'\r\n\r\n // Fast path: entire path only contains safe chars + slashes — return as-is\r\n if (SAFE_PATH_RE.test(path)) return path;\r\n\r\n // Slow path: encode segment by segment\r\n const segments = path.split(\"/\");\r\n for (let i = 0; i < segments.length; i++) {\r\n if (!SAFE_SEGMENT_RE.test(segments[i])) {\r\n segments[i] = encodeURIComponentS3(segments[i]);\r\n }\r\n }\r\n return segments.join(\"/\");\r\n}\r\n","import type { AmzDate } from \"./types.js\";\r\n\r\n// Pre-computed lookup table for zero-padding (0-99)\r\nexport const PAD: string[] = [];\r\nfor (let i = 0; i < 100; i++) {\r\n PAD[i] = i < 10 ? `0${i}` : `${i}`;\r\n}\r\n\r\n/**\r\n * Convert timestamp to AMZ date format\r\n * @param timestamp - Unix timestamp in milliseconds\r\n * @returns AMZ date object with date, dateTime, and numericDay\r\n */\r\nexport function getAmzDate(timestamp: number): AmzDate {\r\n const d = new Date(timestamp);\r\n const year = d.getUTCFullYear();\r\n const month = d.getUTCMonth() + 1;\r\n const day = d.getUTCDate();\r\n const hours = d.getUTCHours();\r\n const minutes = d.getUTCMinutes();\r\n const seconds = d.getUTCSeconds();\r\n\r\n const date = `${year}${PAD[month]}${PAD[day]}`;\r\n const dateTime = `${date}T${PAD[hours]}${PAD[minutes]}${PAD[seconds]}Z`;\r\n\r\n return { date, dateTime, timestamp };\r\n}\r\n\r\n// Cache for sub-second date reuse\r\nlet cachedSecond = 0;\r\nlet cachedDate: AmzDate | null = null;\r\n\r\n/**\r\n * Get current AMZ date with sub-second caching\r\n * @returns Current AMZ date\r\n */\r\nexport function now(): AmzDate {\r\n const currentSecond = Math.floor(Date.now() / 1000);\r\n if (currentSecond === cachedSecond && cachedDate !== null) {\r\n return cachedDate;\r\n }\r\n const computed = getAmzDate(currentSecond * 1000);\r\n cachedSecond = currentSecond;\r\n cachedDate = computed;\r\n return cachedDate;\r\n}\r\n","import { createHmac, hash as cryptoHash } from \"node:crypto\";\r\n\r\n/**\r\n * Derive AWS SigV4 signing key\r\n */\r\nexport function deriveSigningKey(\r\n secretAccessKey: string,\r\n date: string,\r\n region: string\r\n): Buffer {\r\n const kDate = createHmac(\"sha256\", `AWS4${secretAccessKey}`)\r\n .update(date)\r\n .digest();\r\n const kRegion = createHmac(\"sha256\", kDate).update(region).digest();\r\n const kService = createHmac(\"sha256\", kRegion).update(\"s3\").digest();\r\n const kSigning = createHmac(\"sha256\", kService)\r\n .update(\"aws4_request\")\r\n .digest();\r\n return kSigning;\r\n}\r\n\r\n/**\r\n * Hash canonical request (host-only fast path)\r\n */\r\nexport function hashCanonicalRequest(\r\n method: string,\r\n path: string,\r\n query: string,\r\n host: string\r\n): string {\r\n // host is the only signed header in the fast path\r\n const canonical =\r\n `${method}\\n${path}\\n${query}\\nhost:${host}\\n\\nhost\\nUNSIGNED-PAYLOAD`;\r\n return cryptoHash(\"sha256\", canonical, \"hex\");\r\n}\r\n\r\n/**\r\n * Hash canonical request with additional headers\r\n */\r\nexport function hashCanonicalRequestGeneric(\r\n method: string,\r\n path: string,\r\n query: string,\r\n headers: Record<string, string>,\r\n sortedKeys: readonly string[],\r\n signedHeaders: string\r\n): string {\r\n let headerLines = \"\";\r\n for (let i = 0; i < sortedKeys.length; i++) {\r\n const key = sortedKeys[i];\r\n headerLines += `${key}:${headers[key]}\\n`;\r\n }\r\n\r\n const canonical =\r\n `${method}\\n${path}\\n${query}\\n${headerLines}\\n${signedHeaders}\\nUNSIGNED-PAYLOAD`;\r\n return cryptoHash(\"sha256\", canonical, \"hex\");\r\n}\r\n\r\n\r\n/**\r\n * Cache for signing keys (daily invalidation)\r\n */\r\nexport class SigningKeyCache {\r\n private cachedKey: Buffer | null = null;\r\n private cachedDate = \"\";\r\n\r\n constructor(\r\n private readonly region: string,\r\n private readonly secretAccessKey: string\r\n ) {}\r\n\r\n get(date: string): Buffer {\r\n if (date === this.cachedDate && this.cachedKey) {\r\n return this.cachedKey;\r\n }\r\n this.cachedDate = date;\r\n this.cachedKey = deriveSigningKey(this.secretAccessKey, date, this.region);\r\n return this.cachedKey;\r\n }\r\n}\r\n","/** Numeric error codes for fast comparison */\r\nexport const S3ErrorCode = {\r\n MISSING_CONFIG: 1000,\r\n INVALID_BUCKET: 1001,\r\n MISSING_KEY: 1002,\r\n INVALID_EXPIRATION: 1003,\r\n INVALID_CONTENT_LENGTH: 1004,\r\n} as const;\r\n\r\nexport type S3ErrorCode = typeof S3ErrorCode[keyof typeof S3ErrorCode];\r\nexport class S3Error extends Error {\r\n constructor(message: string, public readonly code: S3ErrorCode) {\r\n super(message);\r\n this.name = \"S3Error\";\r\n if (Error.captureStackTrace) {\r\n Error.captureStackTrace(this, S3Error);\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["index_exports","__export","S3Error","S3ErrorCode","S3Presigner","__toCommonJS","import_node_crypto","SAFE_PATH_RE","SAFE_SEGMENT_RE","ENCODE_RE","ENCODE_MAP","encodeURIComponentS3","value","c","encodeS3Path","path","segments","i","PAD","i","getAmzDate","timestamp","d","year","month","day","hours","minutes","seconds","date","dateTime","cachedSecond","cachedDate","now","currentSecond","computed","import_node_crypto","deriveSigningKey","secretAccessKey","date","region","kDate","kRegion","kService","hashCanonicalRequest","method","path","query","host","canonical","cryptoHash","hashCanonicalRequestGeneric","headers","sortedKeys","signedHeaders","headerLines","i","key","SigningKeyCache","S3ErrorCode","S3Error","_S3Error","message","code","S3Presigner","config","S3Error","S3ErrorCode","SigningKeyCache","endpoint","protocol","slashIdx","hostPart","endpointPath","isLocalhost","isIP","usePathStyle","encodeURIComponentS3","bucket","key","options","method","expiresIn","date","now","encodedPath","encodeS3Path","contentType","contentLength","additionalSignedHeaders","acl","responseContentDisposition","responseContentType","storageClass","hasExtra","headers","signedHeaders","sortedHeaderKeys","k","v","lowerK","normalizedV","credentialPart","hasOptional","query","params","a","b","i","canonicalHash","hashCanonicalRequestGeneric","hashCanonicalRequest","stringToSign","signingKey","signature","expDate","expiration","PAD","credential","conditions","cond","policy","encodedPolicy","fields"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** HTTP methods supported for S3 presigned URLs */
|
|
2
|
+
type PresignMethod = "GET" | "PUT" | "DELETE" | "HEAD";
|
|
3
|
+
/** Configuration for S3Presigner instance */
|
|
4
|
+
interface S3PresignerConfig {
|
|
5
|
+
/** S3 endpoint URL (e.g., "https://my-bucket.s3.us-east-1.amazonaws.com" or "http://localhost:9000") */
|
|
6
|
+
readonly endpoint: string;
|
|
7
|
+
/** AWS region (e.g., "us-east-1") */
|
|
8
|
+
readonly region: string;
|
|
9
|
+
/** S3 bucket name */
|
|
10
|
+
readonly bucket: string;
|
|
11
|
+
/** AWS access key ID */
|
|
12
|
+
readonly accessKeyId: string;
|
|
13
|
+
/** AWS secret access key */
|
|
14
|
+
readonly secretAccessKey: string;
|
|
15
|
+
/** AWS session token (for temporary credentials) */
|
|
16
|
+
readonly sessionToken?: string;
|
|
17
|
+
/** Force path-style URLs (bucket in path vs subdomain) */
|
|
18
|
+
readonly forcePathStyle?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Options for presigning a URL */
|
|
21
|
+
interface PresignOptions {
|
|
22
|
+
/** HTTP method (default: "GET") */
|
|
23
|
+
readonly method?: PresignMethod;
|
|
24
|
+
/** Expiration time in seconds (default: 3600, max: 604800) */
|
|
25
|
+
readonly expiresIn?: number;
|
|
26
|
+
/** Content-Type header to include in signature */
|
|
27
|
+
readonly contentType?: string;
|
|
28
|
+
/** Content-Length header to include in signature */
|
|
29
|
+
readonly contentLength?: number;
|
|
30
|
+
/** S3 ACL (e.g., "public-read", "private") */
|
|
31
|
+
readonly acl?: string;
|
|
32
|
+
/** S3 storage class (e.g., "STANDARD", "GLACIER") */
|
|
33
|
+
readonly storageClass?: string;
|
|
34
|
+
/** Response Content-Disposition header */
|
|
35
|
+
readonly responseContentDisposition?: string;
|
|
36
|
+
/** Response Content-Type header */
|
|
37
|
+
readonly responseContentType?: string;
|
|
38
|
+
/** Additional headers to include in signature */
|
|
39
|
+
readonly additionalSignedHeaders?: Readonly<Record<string, string>>;
|
|
40
|
+
}
|
|
41
|
+
/** Options for POST presigning */
|
|
42
|
+
interface PresignPostOptions {
|
|
43
|
+
/** Expiration time in seconds (default: 3600, max: 604800) */
|
|
44
|
+
readonly expiresIn?: number;
|
|
45
|
+
/** S3 ACL (e.g., "public-read", "private") */
|
|
46
|
+
readonly acl?: string;
|
|
47
|
+
/** S3 storage class (e.g., "STANDARD", "GLACIER") */
|
|
48
|
+
readonly storageClass?: string;
|
|
49
|
+
/** Content-Type header */
|
|
50
|
+
readonly contentType?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Additional form fields to include in the POST request.
|
|
53
|
+
* Note: signing fields (Policy, X-Amz-Signature, X-Amz-Algorithm, X-Amz-Credential,
|
|
54
|
+
* X-Amz-Date, key) are always set by the library and will not be overwritten by this option.
|
|
55
|
+
*/
|
|
56
|
+
readonly fields?: Readonly<Record<string, string>>;
|
|
57
|
+
/** Conditions array for the policy */
|
|
58
|
+
readonly conditions?: readonly (readonly string[] | readonly number[] | Readonly<Record<string, string>>)[];
|
|
59
|
+
}
|
|
60
|
+
/** Result of POST presigning */
|
|
61
|
+
interface PresignPostResult {
|
|
62
|
+
/** S3 endpoint URL for POST request */
|
|
63
|
+
readonly url: string;
|
|
64
|
+
/** Form fields to include in multipart POST */
|
|
65
|
+
readonly fields: Record<string, string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
declare class S3Presigner {
|
|
69
|
+
private readonly region;
|
|
70
|
+
private readonly accessKeyId;
|
|
71
|
+
private readonly bucket;
|
|
72
|
+
private readonly signingKeyCache;
|
|
73
|
+
private readonly urlPrefix;
|
|
74
|
+
private readonly endpointPath;
|
|
75
|
+
private readonly pathPrefix;
|
|
76
|
+
private readonly host;
|
|
77
|
+
private readonly scope;
|
|
78
|
+
private readonly credentialScopeEncoded;
|
|
79
|
+
private readonly credentialPrefix;
|
|
80
|
+
private readonly sessionToken;
|
|
81
|
+
constructor(config: S3PresignerConfig);
|
|
82
|
+
private validateBucketName;
|
|
83
|
+
presign(key: string, options?: PresignOptions): string;
|
|
84
|
+
presignPost(key: string, options?: PresignPostOptions): PresignPostResult;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Numeric error codes for fast comparison */
|
|
88
|
+
declare const S3ErrorCode: {
|
|
89
|
+
readonly MISSING_CONFIG: 1000;
|
|
90
|
+
readonly INVALID_BUCKET: 1001;
|
|
91
|
+
readonly MISSING_KEY: 1002;
|
|
92
|
+
readonly INVALID_EXPIRATION: 1003;
|
|
93
|
+
readonly INVALID_CONTENT_LENGTH: 1004;
|
|
94
|
+
};
|
|
95
|
+
type S3ErrorCode = typeof S3ErrorCode[keyof typeof S3ErrorCode];
|
|
96
|
+
declare class S3Error extends Error {
|
|
97
|
+
readonly code: S3ErrorCode;
|
|
98
|
+
constructor(message: string, code: S3ErrorCode);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { type PresignMethod, type PresignOptions, type PresignPostOptions, type PresignPostResult, S3Error, S3ErrorCode, S3Presigner, type S3PresignerConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** HTTP methods supported for S3 presigned URLs */
|
|
2
|
+
type PresignMethod = "GET" | "PUT" | "DELETE" | "HEAD";
|
|
3
|
+
/** Configuration for S3Presigner instance */
|
|
4
|
+
interface S3PresignerConfig {
|
|
5
|
+
/** S3 endpoint URL (e.g., "https://my-bucket.s3.us-east-1.amazonaws.com" or "http://localhost:9000") */
|
|
6
|
+
readonly endpoint: string;
|
|
7
|
+
/** AWS region (e.g., "us-east-1") */
|
|
8
|
+
readonly region: string;
|
|
9
|
+
/** S3 bucket name */
|
|
10
|
+
readonly bucket: string;
|
|
11
|
+
/** AWS access key ID */
|
|
12
|
+
readonly accessKeyId: string;
|
|
13
|
+
/** AWS secret access key */
|
|
14
|
+
readonly secretAccessKey: string;
|
|
15
|
+
/** AWS session token (for temporary credentials) */
|
|
16
|
+
readonly sessionToken?: string;
|
|
17
|
+
/** Force path-style URLs (bucket in path vs subdomain) */
|
|
18
|
+
readonly forcePathStyle?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Options for presigning a URL */
|
|
21
|
+
interface PresignOptions {
|
|
22
|
+
/** HTTP method (default: "GET") */
|
|
23
|
+
readonly method?: PresignMethod;
|
|
24
|
+
/** Expiration time in seconds (default: 3600, max: 604800) */
|
|
25
|
+
readonly expiresIn?: number;
|
|
26
|
+
/** Content-Type header to include in signature */
|
|
27
|
+
readonly contentType?: string;
|
|
28
|
+
/** Content-Length header to include in signature */
|
|
29
|
+
readonly contentLength?: number;
|
|
30
|
+
/** S3 ACL (e.g., "public-read", "private") */
|
|
31
|
+
readonly acl?: string;
|
|
32
|
+
/** S3 storage class (e.g., "STANDARD", "GLACIER") */
|
|
33
|
+
readonly storageClass?: string;
|
|
34
|
+
/** Response Content-Disposition header */
|
|
35
|
+
readonly responseContentDisposition?: string;
|
|
36
|
+
/** Response Content-Type header */
|
|
37
|
+
readonly responseContentType?: string;
|
|
38
|
+
/** Additional headers to include in signature */
|
|
39
|
+
readonly additionalSignedHeaders?: Readonly<Record<string, string>>;
|
|
40
|
+
}
|
|
41
|
+
/** Options for POST presigning */
|
|
42
|
+
interface PresignPostOptions {
|
|
43
|
+
/** Expiration time in seconds (default: 3600, max: 604800) */
|
|
44
|
+
readonly expiresIn?: number;
|
|
45
|
+
/** S3 ACL (e.g., "public-read", "private") */
|
|
46
|
+
readonly acl?: string;
|
|
47
|
+
/** S3 storage class (e.g., "STANDARD", "GLACIER") */
|
|
48
|
+
readonly storageClass?: string;
|
|
49
|
+
/** Content-Type header */
|
|
50
|
+
readonly contentType?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Additional form fields to include in the POST request.
|
|
53
|
+
* Note: signing fields (Policy, X-Amz-Signature, X-Amz-Algorithm, X-Amz-Credential,
|
|
54
|
+
* X-Amz-Date, key) are always set by the library and will not be overwritten by this option.
|
|
55
|
+
*/
|
|
56
|
+
readonly fields?: Readonly<Record<string, string>>;
|
|
57
|
+
/** Conditions array for the policy */
|
|
58
|
+
readonly conditions?: readonly (readonly string[] | readonly number[] | Readonly<Record<string, string>>)[];
|
|
59
|
+
}
|
|
60
|
+
/** Result of POST presigning */
|
|
61
|
+
interface PresignPostResult {
|
|
62
|
+
/** S3 endpoint URL for POST request */
|
|
63
|
+
readonly url: string;
|
|
64
|
+
/** Form fields to include in multipart POST */
|
|
65
|
+
readonly fields: Record<string, string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
declare class S3Presigner {
|
|
69
|
+
private readonly region;
|
|
70
|
+
private readonly accessKeyId;
|
|
71
|
+
private readonly bucket;
|
|
72
|
+
private readonly signingKeyCache;
|
|
73
|
+
private readonly urlPrefix;
|
|
74
|
+
private readonly endpointPath;
|
|
75
|
+
private readonly pathPrefix;
|
|
76
|
+
private readonly host;
|
|
77
|
+
private readonly scope;
|
|
78
|
+
private readonly credentialScopeEncoded;
|
|
79
|
+
private readonly credentialPrefix;
|
|
80
|
+
private readonly sessionToken;
|
|
81
|
+
constructor(config: S3PresignerConfig);
|
|
82
|
+
private validateBucketName;
|
|
83
|
+
presign(key: string, options?: PresignOptions): string;
|
|
84
|
+
presignPost(key: string, options?: PresignPostOptions): PresignPostResult;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Numeric error codes for fast comparison */
|
|
88
|
+
declare const S3ErrorCode: {
|
|
89
|
+
readonly MISSING_CONFIG: 1000;
|
|
90
|
+
readonly INVALID_BUCKET: 1001;
|
|
91
|
+
readonly MISSING_KEY: 1002;
|
|
92
|
+
readonly INVALID_EXPIRATION: 1003;
|
|
93
|
+
readonly INVALID_CONTENT_LENGTH: 1004;
|
|
94
|
+
};
|
|
95
|
+
type S3ErrorCode = typeof S3ErrorCode[keyof typeof S3ErrorCode];
|
|
96
|
+
declare class S3Error extends Error {
|
|
97
|
+
readonly code: S3ErrorCode;
|
|
98
|
+
constructor(message: string, code: S3ErrorCode);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { type PresignMethod, type PresignOptions, type PresignPostOptions, type PresignPostResult, S3Error, S3ErrorCode, S3Presigner, type S3PresignerConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import{createHmac as H}from"crypto";var L=/^[A-Za-z0-9\-._~/]+$/,F=/^[A-Za-z0-9\-._~]*$/,B=/[!'()*]/g,W={"!":"%21","'":"%27","(":"%28",")":"%29","*":"%2A"};function A(s){return encodeURIComponent(s).replace(B,e=>W[e])}function O(s){if(s.charCodeAt(0)===47&&(s=s.slice(1)),L.test(s))return s;let e=s.split("/");for(let t=0;t<e.length;t++)F.test(e[t])||(e[t]=A(e[t]));return e.join("/")}var p=[];for(let s=0;s<100;s++)p[s]=s<10?`0${s}`:`${s}`;function V(s){let e=new Date(s),t=e.getUTCFullYear(),o=e.getUTCMonth()+1,r=e.getUTCDate(),n=e.getUTCHours(),d=e.getUTCMinutes(),u=e.getUTCSeconds(),i=`${t}${p[o]}${p[r]}`,g=`${i}T${p[n]}${p[d]}${p[u]}Z`;return{date:i,dateTime:g,timestamp:s}}var _=0,x=null;function z(){let s=Math.floor(Date.now()/1e3);if(s===_&&x!==null)return x;let e=V(s*1e3);return _=s,x=e,x}import{createHmac as N,hash as v}from"crypto";function j(s,e,t){let o=N("sha256",`AWS4${s}`).update(e).digest(),r=N("sha256",o).update(t).digest(),n=N("sha256",r).update("s3").digest();return N("sha256",n).update("aws4_request").digest()}function G(s,e,t,o){let r=`${s}
|
|
2
|
+
${e}
|
|
3
|
+
${t}
|
|
4
|
+
host:${o}
|
|
5
|
+
|
|
6
|
+
host
|
|
7
|
+
UNSIGNED-PAYLOAD`;return v("sha256",r,"hex")}function b(s,e,t,o,r,n){let d="";for(let i=0;i<r.length;i++){let g=r[i];d+=`${g}:${o[g]}
|
|
8
|
+
`}let u=`${s}
|
|
9
|
+
${e}
|
|
10
|
+
${t}
|
|
11
|
+
${d}
|
|
12
|
+
${n}
|
|
13
|
+
UNSIGNED-PAYLOAD`;return v("sha256",u,"hex")}var E=class{constructor(e,t){this.region=e;this.secretAccessKey=t}cachedKey=null;cachedDate="";get(e){return e===this.cachedDate&&this.cachedKey?this.cachedKey:(this.cachedDate=e,this.cachedKey=j(this.secretAccessKey,e,this.region),this.cachedKey)}};var h={MISSING_CONFIG:1e3,INVALID_BUCKET:1001,MISSING_KEY:1002,INVALID_EXPIRATION:1003,INVALID_CONTENT_LENGTH:1004},a=class s extends Error{constructor(t,o){super(t);this.code=o;this.name="S3Error",Error.captureStackTrace&&Error.captureStackTrace(this,s)}};var k=class{region;accessKeyId;bucket;signingKeyCache;urlPrefix;endpointPath;pathPrefix;host;scope;credentialScopeEncoded;credentialPrefix;sessionToken;constructor(e){if(!e.endpoint?.trim())throw new a("endpoint is required",h.MISSING_CONFIG);if(!e.region?.trim())throw new a("region is required",h.MISSING_CONFIG);if(!e.bucket?.trim())throw new a("bucket is required",h.MISSING_CONFIG);if(!e.accessKeyId?.trim())throw new a("accessKeyId is required",h.MISSING_CONFIG);if(!e.secretAccessKey?.trim())throw new a("secretAccessKey is required",h.MISSING_CONFIG);this.validateBucketName(e.bucket),this.region=e.region,this.accessKeyId=e.accessKeyId,this.bucket=e.bucket,this.signingKeyCache=new E(e.region,e.secretAccessKey),this.sessionToken=e.sessionToken||"";let t=e.endpoint,o=t.startsWith("https")?"https://":"http://";t.startsWith("https://")?t=t.slice(8):t.startsWith("http://")&&(t=t.slice(7)),t.endsWith("/")&&(t=t.slice(0,-1));let r=t.indexOf("/"),n,d;r!==-1?(n=t.slice(0,r),d=t.slice(r)):(n=t,d="");let u=n.startsWith("localhost"),i=/^\d+\.\d+\.\d+\.\d+(?::\d+)?$/.test(n),g=e.forcePathStyle??(u||i);g||(n=e.bucket+"."+n),this.host=n,this.endpointPath=d,this.urlPrefix=o+n,this.pathPrefix=g?e.bucket+"/":"",this.credentialPrefix=A(e.accessKeyId)+"%2F",this.credentialScopeEncoded="%2F"+this.region+"%2Fs3%2Faws4_request",this.scope=e.region+"/s3/aws4_request"}validateBucketName(e){if(e.length<3||e.length>63||!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(e))throw new a("Bucket name must be 3-63 characters, lowercase letters, digits, hyphens, and periods",h.INVALID_BUCKET);if(e.includes("..")||e.includes(".-")||e.includes("-."))throw new a("Invalid bucket name: consecutive periods or period-dash",h.INVALID_BUCKET);if(/^\d+\.\d+\.\d+\.\d+$/.test(e))throw new a("Bucket name must not be formatted as an IP address",h.INVALID_BUCKET)}presign(e,t){if(!e)throw new a("key is required",h.MISSING_KEY);let o=t?.method??"GET",r=t?.expiresIn??3600;if(!Number.isFinite(r)||r<1||r>604800||r!==Math.floor(r))throw new a("expiresIn must be an integer between 1 and 604800 seconds",h.INVALID_EXPIRATION);let n=z(),d=this.endpointPath+"/"+this.pathPrefix+O(e),{contentType:u,contentLength:i,additionalSignedHeaders:g,acl:y,responseContentDisposition:C,responseContentType:P,storageClass:c}=t??{},D=!!(u||i!==void 0||g),f,T,w;if(D){if(f=Object.create(null),f.host=this.host,u&&(f["content-type"]=u),i!==void 0){if(!Number.isSafeInteger(i)||i<0)throw new a("contentLength must be a non-negative integer",h.INVALID_CONTENT_LENGTH);f["content-length"]=String(i)}if(g)for(let[l,m]of Object.entries(g)){let S=l.toLowerCase();if(S==="host"||S==="content-type"||S==="content-length")continue;let K=m.trim().replace(/\s+/g," ");f[S]!==void 0?f[S]+=","+K:f[S]=K}w=Object.keys(f).sort(),T=w.join(";")}else T="host";let $=this.credentialPrefix+n.date+this.credentialScopeEncoded,M=!!(y||C||P||c||this.sessionToken),I;if(!M)I="X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential="+$+"&X-Amz-Date="+n.dateTime+"&X-Amz-Expires="+r+"&X-Amz-SignedHeaders="+T;else{let l=[["X-Amz-Algorithm","AWS4-HMAC-SHA256"],["X-Amz-Content-Sha256","UNSIGNED-PAYLOAD"],["X-Amz-Credential",$],["X-Amz-Date",n.dateTime],["X-Amz-Expires",String(r)],["X-Amz-SignedHeaders",T]];y&&l.push(["X-Amz-Acl",A(y)]),C&&l.push(["response-content-disposition",A(C)]),P&&l.push(["response-content-type",A(P)]),c&&l.push(["X-Amz-Storage-Class",A(c)]),this.sessionToken&&l.push(["X-Amz-Security-Token",A(this.sessionToken)]),l.sort((m,S)=>m[0]<S[0]?-1:m[0]>S[0]?1:0),I=l[0][0]+"="+l[0][1];for(let m=1;m<l.length;m++)I+="&"+l[m][0]+"="+l[m][1]}let R=f?b(o,d,I,f,w,T):G(o,d,I,this.host),X=`AWS4-HMAC-SHA256
|
|
14
|
+
`+n.dateTime+`
|
|
15
|
+
`+n.date+"/"+this.scope+`
|
|
16
|
+
`+R,q=this.signingKeyCache.get(n.date),U=H("sha256",q).update(X).digest("hex");return this.urlPrefix+d+"?"+I+"&X-Amz-Signature="+U}presignPost(e,t){if(!e)throw new a("key is required",h.MISSING_KEY);let o=t?.expiresIn??3600;if(!Number.isFinite(o)||o<1||o>604800||o!==Math.floor(o))throw new a("expiresIn must be an integer between 1 and 604800 seconds",h.INVALID_EXPIRATION);let r=z(),n=new Date(r.timestamp+o*1e3),d=n.getUTCFullYear()+"-"+p[n.getUTCMonth()+1]+"-"+p[n.getUTCDate()]+"T"+p[n.getUTCHours()]+":"+p[n.getUTCMinutes()]+":"+p[n.getUTCSeconds()]+"Z",u=this.accessKeyId+"/"+r.date+"/"+this.scope,i=[{bucket:this.bucket},["eq","$key",e],["eq","$x-amz-algorithm","AWS4-HMAC-SHA256"],["eq","$x-amz-credential",u],["eq","$x-amz-date",r.dateTime]];if(this.sessionToken&&i.push(["eq","$x-amz-security-token",this.sessionToken]),t?.acl&&i.push(["eq","$acl",t.acl]),t?.contentType&&i.push(["eq","$content-type",t.contentType]),t?.storageClass&&i.push(["eq","$x-amz-storage-class",t.storageClass]),t?.conditions)for(let D of t.conditions)i.push(D);let g={expiration:d,conditions:i},y=Buffer.from(JSON.stringify(g),"utf8").toString("base64"),C=this.signingKeyCache.get(r.date),P=H("sha256",C).update(y).digest("hex"),c={};return t?.fields&&Object.assign(c,t.fields),c["X-Amz-Algorithm"]="AWS4-HMAC-SHA256",c["X-Amz-Credential"]=u,c["X-Amz-Date"]=r.dateTime,c.key=e,c.Policy=y,c["X-Amz-Signature"]=P,this.sessionToken&&(c["X-Amz-Security-Token"]=this.sessionToken),t?.acl&&(c.acl=t.acl),t?.contentType&&(c["content-type"]=t.contentType),t?.storageClass&&(c["x-amz-storage-class"]=t.storageClass),{url:this.urlPrefix+this.endpointPath+(this.pathPrefix?"/"+this.bucket:""),fields:c}}};export{a as S3Error,h as S3ErrorCode,k as S3Presigner};
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/presigner.ts","../src/encode.ts","../src/date.ts","../src/signing.ts","../src/error.ts"],"sourcesContent":["import { createHmac } from \"node:crypto\";\r\nimport type { S3PresignerConfig, PresignOptions, PresignPostOptions, PresignPostResult } from \"./types.js\";\r\nimport { encodeS3Path, encodeURIComponentS3 } from \"./encode.js\";\r\nimport { now, PAD } from \"./date.js\";\r\nimport { hashCanonicalRequest, hashCanonicalRequestGeneric, SigningKeyCache } from \"./signing.js\";\r\nimport { S3Error, S3ErrorCode } from \"./error.js\";\r\n\r\nexport class S3Presigner {\r\n private readonly region: string;\r\n private readonly accessKeyId: string;\r\n private readonly bucket: string;\r\n private readonly signingKeyCache: SigningKeyCache;\r\n\r\n private readonly urlPrefix: string;\r\n private readonly endpointPath: string;\r\n private readonly pathPrefix: string;\r\n private readonly host: string;\r\n private readonly scope: string;\r\n private readonly credentialScopeEncoded: string;\r\n private readonly credentialPrefix: string;\r\n private readonly sessionToken: string;\r\n\r\n constructor(config: S3PresignerConfig) {\r\n if (!config.endpoint?.trim()) throw new S3Error(\"endpoint is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.region?.trim()) throw new S3Error(\"region is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.bucket?.trim()) throw new S3Error(\"bucket is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.accessKeyId?.trim()) throw new S3Error(\"accessKeyId is required\", S3ErrorCode.MISSING_CONFIG);\r\n if (!config.secretAccessKey?.trim()) throw new S3Error(\"secretAccessKey is required\", S3ErrorCode.MISSING_CONFIG);\r\n\r\n this.validateBucketName(config.bucket);\r\n\r\n this.region = config.region;\r\n this.accessKeyId = config.accessKeyId;\r\n this.bucket = config.bucket;\r\n this.signingKeyCache = new SigningKeyCache(config.region, config.secretAccessKey);\r\n this.sessionToken = config.sessionToken || \"\";\r\n\r\n let endpoint = config.endpoint;\r\n const protocol = endpoint.startsWith(\"https\") ? \"https://\" : \"http://\";\r\n if (endpoint.startsWith(\"https://\")) endpoint = endpoint.slice(8);\r\n else if (endpoint.startsWith(\"http://\")) endpoint = endpoint.slice(7);\r\n if (endpoint.endsWith(\"/\")) endpoint = endpoint.slice(0, -1);\r\n\r\n // Separate host (domain+port) from any path component\r\n const slashIdx = endpoint.indexOf(\"/\");\r\n let hostPart: string;\r\n let endpointPath: string;\r\n if (slashIdx !== -1) {\r\n hostPart = endpoint.slice(0, slashIdx);\r\n endpointPath = endpoint.slice(slashIdx); // includes leading /\r\n } else {\r\n hostPart = endpoint;\r\n endpointPath = \"\";\r\n }\r\n\r\n const isLocalhost = hostPart.startsWith(\"localhost\");\r\n const isIP = /^\\d+\\.\\d+\\.\\d+\\.\\d+(?::\\d+)?$/.test(hostPart);\r\n const usePathStyle = config.forcePathStyle ?? (isLocalhost || isIP);\r\n\r\n if (!usePathStyle) {\r\n hostPart = config.bucket + \".\" + hostPart;\r\n }\r\n\r\n this.host = hostPart;\r\n this.endpointPath = endpointPath;\r\n this.urlPrefix = protocol + hostPart;\r\n this.pathPrefix = usePathStyle ? config.bucket + \"/\" : \"\";\r\n\r\n this.credentialPrefix = encodeURIComponentS3(config.accessKeyId) + \"%2F\";\r\n this.credentialScopeEncoded = \"%2F\" + this.region + \"%2Fs3%2Faws4_request\";\r\n this.scope = config.region + \"/s3/aws4_request\";\r\n }\r\n\r\n private validateBucketName(bucket: string): void {\r\n if (bucket.length < 3 || bucket.length > 63 || !/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(bucket)) {\r\n throw new S3Error(\"Bucket name must be 3-63 characters, lowercase letters, digits, hyphens, and periods\", S3ErrorCode.INVALID_BUCKET);\r\n }\r\n if (bucket.includes(\"..\") || bucket.includes(\".-\") || bucket.includes(\"-.\")) {\r\n throw new S3Error(\"Invalid bucket name: consecutive periods or period-dash\", S3ErrorCode.INVALID_BUCKET);\r\n }\r\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(bucket)) {\r\n throw new S3Error(\"Bucket name must not be formatted as an IP address\", S3ErrorCode.INVALID_BUCKET);\r\n }\r\n }\r\n\r\n presign(key: string, options?: PresignOptions): string {\r\n if (!key) throw new S3Error(\"key is required\", S3ErrorCode.MISSING_KEY);\r\n\r\n const method = options?.method ?? \"GET\";\r\n const expiresIn = options?.expiresIn ?? 3600;\r\n\r\n if (!Number.isFinite(expiresIn) || expiresIn < 1 || expiresIn > 604800 || expiresIn !== Math.floor(expiresIn)) {\r\n throw new S3Error(\"expiresIn must be an integer between 1 and 604800 seconds\", S3ErrorCode.INVALID_EXPIRATION);\r\n }\r\n\r\n const date = now();\r\n const encodedPath = this.endpointPath + \"/\" + this.pathPrefix + encodeS3Path(key);\r\n\r\n const { \r\n contentType, \r\n contentLength, \r\n additionalSignedHeaders, \r\n acl, \r\n responseContentDisposition, \r\n responseContentType, \r\n storageClass \r\n } = options ?? {};\r\n\r\n // Determine signed headers\r\n const hasExtra = !!(contentType || contentLength !== undefined || additionalSignedHeaders);\r\n let headers: Record<string, string> | undefined;\r\n let signedHeaders: string;\r\n let sortedHeaderKeys: string[] | undefined;\r\n\r\n if (hasExtra) {\r\n headers = Object.create(null) as Record<string, string>;\r\n headers[\"host\"] = this.host;\r\n // Do NOT sign x-amz-date in headers - it's already in the query string\r\n if (contentType) headers[\"content-type\"] = contentType;\r\n if (contentLength !== undefined) {\r\n if (!Number.isSafeInteger(contentLength) || contentLength < 0) throw new S3Error(\"contentLength must be a non-negative integer\", S3ErrorCode.INVALID_CONTENT_LENGTH);\r\n headers[\"content-length\"] = String(contentLength);\r\n }\r\n if (additionalSignedHeaders) {\r\n for (const [k, v] of Object.entries(additionalSignedHeaders)) {\r\n const lowerK = k.toLowerCase();\r\n if (lowerK === \"host\") continue; // host is always set by the library\r\n if (lowerK === \"content-type\" || lowerK === \"content-length\") continue; // managed by dedicated options\r\n const normalizedV = v.trim().replace(/\\s+/g, \" \");\r\n if (headers[lowerK] !== undefined) {\r\n headers[lowerK] += \",\" + normalizedV;\r\n } else {\r\n headers[lowerK] = normalizedV;\r\n }\r\n }\r\n }\r\n sortedHeaderKeys = Object.keys(headers).sort();\r\n signedHeaders = sortedHeaderKeys.join(\";\");\r\n } else {\r\n signedHeaders = \"host\";\r\n }\r\n\r\n // Build query string — fast path for the common case (no optional params), sort only when needed\r\n // The 6 fixed params are already in alphabetical order: Algorithm < Content-Sha256 < Credential < Date < Expires < SignedHeaders\r\n const credentialPart = this.credentialPrefix + date.date + this.credentialScopeEncoded;\r\n const hasOptional = !!(acl || responseContentDisposition || responseContentType || storageClass || this.sessionToken);\r\n\r\n let query: string;\r\n if (!hasOptional) {\r\n // Fast path: direct concatenation, no allocation overhead\r\n query = \"X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=\" + credentialPart\r\n + \"&X-Amz-Date=\" + date.dateTime\r\n + \"&X-Amz-Expires=\" + expiresIn\r\n + \"&X-Amz-SignedHeaders=\" + signedHeaders;\r\n } else {\r\n // Slow path: optional params displace positions, must sort to satisfy SigV4\r\n const params: [string, string][] = [\r\n [\"X-Amz-Algorithm\", \"AWS4-HMAC-SHA256\"],\r\n [\"X-Amz-Content-Sha256\", \"UNSIGNED-PAYLOAD\"],\r\n [\"X-Amz-Credential\", credentialPart],\r\n [\"X-Amz-Date\", date.dateTime],\r\n [\"X-Amz-Expires\", String(expiresIn)],\r\n [\"X-Amz-SignedHeaders\", signedHeaders],\r\n ];\r\n if (acl) params.push([\"X-Amz-Acl\", encodeURIComponentS3(acl)]);\r\n if (responseContentDisposition) params.push([\"response-content-disposition\", encodeURIComponentS3(responseContentDisposition)]);\r\n if (responseContentType) params.push([\"response-content-type\", encodeURIComponentS3(responseContentType)]);\r\n if (storageClass) params.push([\"X-Amz-Storage-Class\", encodeURIComponentS3(storageClass)]);\r\n if (this.sessionToken) params.push([\"X-Amz-Security-Token\", encodeURIComponentS3(this.sessionToken)]);\r\n params.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));\r\n query = params[0][0] + \"=\" + params[0][1];\r\n for (let i = 1; i < params.length; i++) {\r\n query += \"&\" + params[i][0] + \"=\" + params[i][1];\r\n }\r\n }\r\n\r\n // Hash canonical request — use shared signing module (no inline duplication)\r\n const canonicalHash = headers\r\n ? hashCanonicalRequestGeneric(method, encodedPath, query, headers, sortedHeaderKeys!, signedHeaders)\r\n : hashCanonicalRequest(method, encodedPath, query, this.host);\r\n\r\n // Sign\r\n const stringToSign = \"AWS4-HMAC-SHA256\\n\" + date.dateTime + \"\\n\" + date.date + \"/\" + this.scope + \"\\n\" + canonicalHash;\r\n const signingKey = this.signingKeyCache.get(date.date);\r\n const signature = createHmac(\"sha256\", signingKey)\r\n .update(stringToSign)\r\n .digest(\"hex\");\r\n\r\n return this.urlPrefix + encodedPath + \"?\" + query + \"&X-Amz-Signature=\" + signature;\r\n }\r\n\r\n presignPost(key: string, options?: PresignPostOptions): PresignPostResult {\r\n if (!key) throw new S3Error(\"key is required\", S3ErrorCode.MISSING_KEY);\r\n\r\n const expiresIn = options?.expiresIn ?? 3600;\r\n if (!Number.isFinite(expiresIn) || expiresIn < 1 || expiresIn > 604800 || expiresIn !== Math.floor(expiresIn)) {\r\n throw new S3Error(\"expiresIn must be an integer between 1 and 604800 seconds\", S3ErrorCode.INVALID_EXPIRATION);\r\n }\r\n\r\n const date = now();\r\n // Build expiration timestamp without toISOString().replace() chain\r\n const expDate = new Date(date.timestamp + expiresIn * 1000);\r\n const expiration =\r\n expDate.getUTCFullYear() +\r\n \"-\" + PAD[expDate.getUTCMonth() + 1] +\r\n \"-\" + PAD[expDate.getUTCDate()] +\r\n \"T\" + PAD[expDate.getUTCHours()] +\r\n \":\" + PAD[expDate.getUTCMinutes()] +\r\n \":\" + PAD[expDate.getUTCSeconds()] + \"Z\";\r\n const credential = this.accessKeyId + \"/\" + date.date + \"/\" + this.scope;\r\n\r\n const conditions: (string[] | number[] | Record<string, string>)[] = [\r\n { bucket: this.bucket }, // fix: use stored bucket name, not derived from pathPrefix/host\r\n [\"eq\", \"$key\", key],\r\n [\"eq\", \"$x-amz-algorithm\", \"AWS4-HMAC-SHA256\"],\r\n [\"eq\", \"$x-amz-credential\", credential],\r\n [\"eq\", \"$x-amz-date\", date.dateTime],\r\n ];\r\n\r\n if (this.sessionToken) {\r\n conditions.push([\"eq\", \"$x-amz-security-token\", this.sessionToken]);\r\n }\r\n\r\n if (options?.acl) conditions.push([\"eq\", \"$acl\", options.acl]);\r\n if (options?.contentType) conditions.push([\"eq\", \"$content-type\", options.contentType]);\r\n if (options?.storageClass) conditions.push([\"eq\", \"$x-amz-storage-class\", options.storageClass]);\r\n if (options?.conditions) {\r\n for (const cond of options.conditions) {\r\n conditions.push(cond as string[] | number[] | Record<string, string>);\r\n }\r\n }\r\n\r\n const policy = { expiration, conditions };\r\n const encodedPolicy = Buffer.from(JSON.stringify(policy), \"utf8\").toString(\"base64\");\r\n\r\n const signingKey = this.signingKeyCache.get(date.date);\r\n const signature = createHmac(\"sha256\", signingKey).update(encodedPolicy).digest(\"hex\");\r\n\r\n const fields: Record<string, string> = {};\r\n // Merge user-provided fields FIRST so that signing fields always take precedence.\r\n // This prevents callers from accidentally overwriting Policy, X-Amz-Signature, etc.\r\n if (options?.fields) Object.assign(fields, options.fields);\r\n\r\n fields[\"X-Amz-Algorithm\"] = \"AWS4-HMAC-SHA256\";\r\n fields[\"X-Amz-Credential\"] = credential;\r\n fields[\"X-Amz-Date\"] = date.dateTime;\r\n fields.key = key;\r\n fields.Policy = encodedPolicy;\r\n fields[\"X-Amz-Signature\"] = signature;\r\n\r\n if (this.sessionToken) fields[\"X-Amz-Security-Token\"] = this.sessionToken;\r\n if (options?.acl) fields.acl = options.acl;\r\n if (options?.contentType) fields[\"content-type\"] = options.contentType;\r\n if (options?.storageClass) fields[\"x-amz-storage-class\"] = options.storageClass;\r\n\r\n return {\r\n // fix: virtual-hosted style no longer appends a spurious trailing slash\r\n url: this.urlPrefix + this.endpointPath + (this.pathPrefix ? \"/\" + this.bucket : \"\"),\r\n fields,\r\n };\r\n }\r\n}\r\n","// Fast regex for checking if an entire path needs encoding (safe chars + slashes)\r\nconst SAFE_PATH_RE = /^[A-Za-z0-9\\-._~/]+$/;\r\nconst SAFE_SEGMENT_RE = /^[A-Za-z0-9\\-._~]*$/;\r\n\r\n// Single-pass encoding map for S3-specific characters not covered by encodeURIComponent\r\nconst ENCODE_RE = /[!'()*]/g;\r\nconst ENCODE_MAP: Record<string, string> = {\r\n \"!\": \"%21\",\r\n \"'\": \"%27\",\r\n \"(\": \"%28\",\r\n \")\": \"%29\",\r\n \"*\": \"%2A\",\r\n};\r\n\r\n/**\r\n * Encode a URI component with S3-specific requirements.\r\n * Encodes: ! ' ( ) * in addition to standard encodeURIComponent.\r\n * Uses a single .replace() pass instead of five chained calls.\r\n */\r\nexport function encodeURIComponentS3(value: string): string {\r\n return encodeURIComponent(value).replace(ENCODE_RE, c => ENCODE_MAP[c]);\r\n}\r\n\r\n/**\r\n * Encode S3 object key path.\r\n * Preserves forward slashes, encodes segments as needed.\r\n */\r\nexport function encodeS3Path(path: string): string {\r\n // Strip leading slash\r\n if (path.charCodeAt(0) === 47) path = path.slice(1); // 47 = '/'\r\n\r\n // Fast path: entire path only contains safe chars + slashes — return as-is\r\n if (SAFE_PATH_RE.test(path)) return path;\r\n\r\n // Slow path: encode segment by segment\r\n const segments = path.split(\"/\");\r\n for (let i = 0; i < segments.length; i++) {\r\n if (!SAFE_SEGMENT_RE.test(segments[i])) {\r\n segments[i] = encodeURIComponentS3(segments[i]);\r\n }\r\n }\r\n return segments.join(\"/\");\r\n}\r\n","import type { AmzDate } from \"./types.js\";\r\n\r\n// Pre-computed lookup table for zero-padding (0-99)\r\nexport const PAD: string[] = [];\r\nfor (let i = 0; i < 100; i++) {\r\n PAD[i] = i < 10 ? `0${i}` : `${i}`;\r\n}\r\n\r\n/**\r\n * Convert timestamp to AMZ date format\r\n * @param timestamp - Unix timestamp in milliseconds\r\n * @returns AMZ date object with date, dateTime, and numericDay\r\n */\r\nexport function getAmzDate(timestamp: number): AmzDate {\r\n const d = new Date(timestamp);\r\n const year = d.getUTCFullYear();\r\n const month = d.getUTCMonth() + 1;\r\n const day = d.getUTCDate();\r\n const hours = d.getUTCHours();\r\n const minutes = d.getUTCMinutes();\r\n const seconds = d.getUTCSeconds();\r\n\r\n const date = `${year}${PAD[month]}${PAD[day]}`;\r\n const dateTime = `${date}T${PAD[hours]}${PAD[minutes]}${PAD[seconds]}Z`;\r\n\r\n return { date, dateTime, timestamp };\r\n}\r\n\r\n// Cache for sub-second date reuse\r\nlet cachedSecond = 0;\r\nlet cachedDate: AmzDate | null = null;\r\n\r\n/**\r\n * Get current AMZ date with sub-second caching\r\n * @returns Current AMZ date\r\n */\r\nexport function now(): AmzDate {\r\n const currentSecond = Math.floor(Date.now() / 1000);\r\n if (currentSecond === cachedSecond && cachedDate !== null) {\r\n return cachedDate;\r\n }\r\n const computed = getAmzDate(currentSecond * 1000);\r\n cachedSecond = currentSecond;\r\n cachedDate = computed;\r\n return cachedDate;\r\n}\r\n","import { createHmac, hash as cryptoHash } from \"node:crypto\";\r\n\r\n/**\r\n * Derive AWS SigV4 signing key\r\n */\r\nexport function deriveSigningKey(\r\n secretAccessKey: string,\r\n date: string,\r\n region: string\r\n): Buffer {\r\n const kDate = createHmac(\"sha256\", `AWS4${secretAccessKey}`)\r\n .update(date)\r\n .digest();\r\n const kRegion = createHmac(\"sha256\", kDate).update(region).digest();\r\n const kService = createHmac(\"sha256\", kRegion).update(\"s3\").digest();\r\n const kSigning = createHmac(\"sha256\", kService)\r\n .update(\"aws4_request\")\r\n .digest();\r\n return kSigning;\r\n}\r\n\r\n/**\r\n * Hash canonical request (host-only fast path)\r\n */\r\nexport function hashCanonicalRequest(\r\n method: string,\r\n path: string,\r\n query: string,\r\n host: string\r\n): string {\r\n // host is the only signed header in the fast path\r\n const canonical =\r\n `${method}\\n${path}\\n${query}\\nhost:${host}\\n\\nhost\\nUNSIGNED-PAYLOAD`;\r\n return cryptoHash(\"sha256\", canonical, \"hex\");\r\n}\r\n\r\n/**\r\n * Hash canonical request with additional headers\r\n */\r\nexport function hashCanonicalRequestGeneric(\r\n method: string,\r\n path: string,\r\n query: string,\r\n headers: Record<string, string>,\r\n sortedKeys: readonly string[],\r\n signedHeaders: string\r\n): string {\r\n let headerLines = \"\";\r\n for (let i = 0; i < sortedKeys.length; i++) {\r\n const key = sortedKeys[i];\r\n headerLines += `${key}:${headers[key]}\\n`;\r\n }\r\n\r\n const canonical =\r\n `${method}\\n${path}\\n${query}\\n${headerLines}\\n${signedHeaders}\\nUNSIGNED-PAYLOAD`;\r\n return cryptoHash(\"sha256\", canonical, \"hex\");\r\n}\r\n\r\n\r\n/**\r\n * Cache for signing keys (daily invalidation)\r\n */\r\nexport class SigningKeyCache {\r\n private cachedKey: Buffer | null = null;\r\n private cachedDate = \"\";\r\n\r\n constructor(\r\n private readonly region: string,\r\n private readonly secretAccessKey: string\r\n ) {}\r\n\r\n get(date: string): Buffer {\r\n if (date === this.cachedDate && this.cachedKey) {\r\n return this.cachedKey;\r\n }\r\n this.cachedDate = date;\r\n this.cachedKey = deriveSigningKey(this.secretAccessKey, date, this.region);\r\n return this.cachedKey;\r\n }\r\n}\r\n","/** Numeric error codes for fast comparison */\r\nexport const S3ErrorCode = {\r\n MISSING_CONFIG: 1000,\r\n INVALID_BUCKET: 1001,\r\n MISSING_KEY: 1002,\r\n INVALID_EXPIRATION: 1003,\r\n INVALID_CONTENT_LENGTH: 1004,\r\n} as const;\r\n\r\nexport type S3ErrorCode = typeof S3ErrorCode[keyof typeof S3ErrorCode];\r\nexport class S3Error extends Error {\r\n constructor(message: string, public readonly code: S3ErrorCode) {\r\n super(message);\r\n this.name = \"S3Error\";\r\n if (Error.captureStackTrace) {\r\n Error.captureStackTrace(this, S3Error);\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["createHmac","SAFE_PATH_RE","SAFE_SEGMENT_RE","ENCODE_RE","ENCODE_MAP","encodeURIComponentS3","value","c","encodeS3Path","path","segments","i","PAD","i","getAmzDate","timestamp","d","year","month","day","hours","minutes","seconds","date","dateTime","cachedSecond","cachedDate","now","currentSecond","computed","createHmac","cryptoHash","deriveSigningKey","secretAccessKey","date","region","kDate","kRegion","kService","hashCanonicalRequest","method","path","query","host","canonical","hashCanonicalRequestGeneric","headers","sortedKeys","signedHeaders","headerLines","key","SigningKeyCache","S3ErrorCode","S3Error","_S3Error","message","code","S3Presigner","config","S3Error","S3ErrorCode","SigningKeyCache","endpoint","protocol","slashIdx","hostPart","endpointPath","isLocalhost","isIP","usePathStyle","encodeURIComponentS3","bucket","key","options","method","expiresIn","date","now","encodedPath","encodeS3Path","contentType","contentLength","additionalSignedHeaders","acl","responseContentDisposition","responseContentType","storageClass","hasExtra","headers","signedHeaders","sortedHeaderKeys","k","v","lowerK","normalizedV","credentialPart","hasOptional","query","params","a","b","i","canonicalHash","hashCanonicalRequestGeneric","hashCanonicalRequest","stringToSign","signingKey","signature","createHmac","expDate","expiration","PAD","credential","conditions","cond","policy","encodedPolicy","fields"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fast-s3-presigner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A blazing-fast, type-safe S3 presigned URL generator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"require": {
|
|
13
|
+
"types": "./dist/index.d.cts",
|
|
14
|
+
"default": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"bench": "tsx bench/presign.mts",
|
|
27
|
+
"test": "tsx --test test/*.test.ts"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"s3",
|
|
31
|
+
"presign",
|
|
32
|
+
"aws",
|
|
33
|
+
"performance"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20.12.0"
|
|
39
|
+
},
|
|
40
|
+
"packageManager": "pnpm@10.7.1",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@aws-sdk/client-s3": "^3.1019.0",
|
|
43
|
+
"@aws-sdk/s3-request-presigner": "^3.1019.0",
|
|
44
|
+
"@types/aws4": "^1.11.6",
|
|
45
|
+
"@types/node": "^25.5.0",
|
|
46
|
+
"aws4": "^1.13.2",
|
|
47
|
+
"aws4fetch": "^1.0.20",
|
|
48
|
+
"dotenv": "^17.3.1",
|
|
49
|
+
"lean-s3": "^0.9.13",
|
|
50
|
+
"mitata": "^1.0.34",
|
|
51
|
+
"s3mini": "^0.9.2",
|
|
52
|
+
"tsup": "^8.4.0",
|
|
53
|
+
"tsx": "^4.19.2",
|
|
54
|
+
"typescript": "^5.8.2"
|
|
55
|
+
}
|
|
56
|
+
}
|