@hanzo/s3 0.6.4 → 8.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/MAINTAINERS.md +62 -0
- package/README.md +262 -0
- package/README_zh_CN.md +192 -0
- package/dist/esm/AssumeRoleProvider.d.mts +86 -0
- package/dist/esm/AssumeRoleProvider.mjs +183 -0
- package/dist/esm/CredentialProvider.d.mts +22 -0
- package/dist/esm/CredentialProvider.mjs +48 -0
- package/dist/esm/Credentials.d.mts +22 -0
- package/dist/esm/Credentials.mjs +38 -0
- package/dist/esm/IamAwsProvider.d.mts +27 -0
- package/dist/esm/IamAwsProvider.mjs +189 -0
- package/dist/esm/errors.d.mts +82 -0
- package/dist/esm/errors.mjs +117 -0
- package/dist/esm/helpers.d.mts +156 -0
- package/dist/esm/helpers.mjs +218 -0
- package/dist/esm/internal/async.d.mts +9 -0
- package/dist/esm/internal/async.mjs +14 -0
- package/dist/esm/internal/callbackify.d.mts +1 -0
- package/dist/esm/internal/callbackify.mjs +15 -0
- package/dist/esm/internal/client.d.mts +394 -0
- package/dist/esm/internal/client.mjs +3007 -0
- package/dist/esm/internal/copy-conditions.d.mts +10 -0
- package/dist/esm/internal/copy-conditions.mjs +25 -0
- package/dist/esm/internal/extensions.d.mts +18 -0
- package/dist/esm/internal/extensions.mjs +114 -0
- package/dist/esm/internal/helper.d.mts +177 -0
- package/dist/esm/internal/helper.mjs +552 -0
- package/dist/esm/internal/join-host-port.d.mts +11 -0
- package/dist/esm/internal/join-host-port.mjs +23 -0
- package/dist/esm/internal/post-policy.d.mts +17 -0
- package/dist/esm/internal/post-policy.mjs +98 -0
- package/dist/esm/internal/request.d.mts +11 -0
- package/dist/esm/internal/request.mjs +75 -0
- package/dist/esm/internal/response.d.mts +8 -0
- package/dist/esm/internal/response.mjs +16 -0
- package/dist/esm/internal/s3-endpoints.d.mts +38 -0
- package/dist/esm/internal/s3-endpoints.mjs +68 -0
- package/dist/esm/internal/type.d.mts +482 -0
- package/dist/esm/internal/type.mjs +30 -0
- package/dist/esm/internal/xml-parser.d.mts +93 -0
- package/dist/esm/internal/xml-parser.mjs +819 -0
- package/dist/esm/notification.d.mts +58 -0
- package/dist/esm/notification.mjs +209 -0
- package/dist/esm/s3.d.mts +40 -0
- package/dist/esm/s3.mjs +86 -0
- package/dist/esm/signing.d.mts +5 -0
- package/dist/esm/signing.mjs +258 -0
- package/dist/main/AssumeRoleProvider.d.ts +86 -0
- package/dist/main/AssumeRoleProvider.js +191 -0
- package/dist/main/CredentialProvider.d.ts +22 -0
- package/dist/main/CredentialProvider.js +55 -0
- package/dist/main/Credentials.d.ts +22 -0
- package/dist/main/Credentials.js +45 -0
- package/dist/main/IamAwsProvider.d.ts +27 -0
- package/dist/main/IamAwsProvider.js +198 -0
- package/dist/main/errors.d.ts +82 -0
- package/dist/main/errors.js +138 -0
- package/dist/main/helpers.d.ts +156 -0
- package/dist/main/helpers.js +233 -0
- package/dist/main/internal/async.d.ts +9 -0
- package/dist/main/internal/async.js +24 -0
- package/dist/main/internal/callbackify.d.ts +1 -0
- package/dist/main/internal/callbackify.js +21 -0
- package/dist/main/internal/client.d.ts +394 -0
- package/dist/main/internal/client.js +3014 -0
- package/dist/main/internal/copy-conditions.d.ts +10 -0
- package/dist/main/internal/copy-conditions.js +31 -0
- package/dist/main/internal/extensions.d.ts +18 -0
- package/dist/main/internal/extensions.js +122 -0
- package/dist/main/internal/helper.d.ts +177 -0
- package/dist/main/internal/helper.js +608 -0
- package/dist/main/internal/join-host-port.d.ts +11 -0
- package/dist/main/internal/join-host-port.js +29 -0
- package/dist/main/internal/post-policy.d.ts +17 -0
- package/dist/main/internal/post-policy.js +107 -0
- package/dist/main/internal/request.d.ts +11 -0
- package/dist/main/internal/request.js +83 -0
- package/dist/main/internal/response.d.ts +8 -0
- package/dist/main/internal/response.js +24 -0
- package/dist/main/internal/s3-endpoints.d.ts +38 -0
- package/dist/main/internal/s3-endpoints.js +73 -0
- package/dist/main/internal/type.d.ts +482 -0
- package/dist/main/internal/type.js +42 -0
- package/dist/main/internal/xml-parser.d.ts +93 -0
- package/dist/main/internal/xml-parser.js +849 -0
- package/dist/main/notification.d.ts +58 -0
- package/dist/main/notification.js +230 -0
- package/dist/main/s3.d.ts +40 -0
- package/dist/main/s3.js +117 -0
- package/dist/main/signing.d.ts +5 -0
- package/dist/main/signing.js +269 -0
- package/package.json +146 -39
- package/src/AssumeRoleProvider.ts +262 -0
- package/src/CredentialProvider.ts +54 -0
- package/src/Credentials.ts +44 -0
- package/src/IamAwsProvider.ts +234 -0
- package/src/errors.ts +120 -0
- package/src/helpers.ts +354 -0
- package/src/internal/async.ts +14 -0
- package/src/internal/callbackify.ts +19 -0
- package/src/internal/client.ts +3412 -0
- package/src/internal/copy-conditions.ts +30 -0
- package/src/internal/extensions.ts +140 -0
- package/src/internal/helper.ts +606 -0
- package/src/internal/join-host-port.ts +23 -0
- package/src/internal/post-policy.ts +99 -0
- package/src/internal/request.ts +102 -0
- package/src/internal/response.ts +26 -0
- package/src/internal/s3-endpoints.ts +70 -0
- package/src/internal/type.ts +577 -0
- package/src/internal/xml-parser.ts +871 -0
- package/src/notification.ts +254 -0
- package/src/s3.ts +155 -0
- package/src/signing.ts +325 -0
- package/lib/index.js +0 -450
- package/lib/index.js.map +0 -7
- package/lib/perfTest.js +0 -91
- package/lib/perfTest.js.map +0 -7
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as http from 'node:http'
|
|
2
|
+
import * as https from 'node:https'
|
|
3
|
+
import { URL, URLSearchParams } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { CredentialProvider } from './CredentialProvider.ts'
|
|
6
|
+
import { Credentials } from './Credentials.ts'
|
|
7
|
+
import { makeDateLong, parseXml, toSha256 } from './internal/helper.ts'
|
|
8
|
+
import { request } from './internal/request.ts'
|
|
9
|
+
import { readAsString } from './internal/response.ts'
|
|
10
|
+
import type { Transport } from './internal/type.ts'
|
|
11
|
+
import { signV4ByServiceName } from './signing.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
|
15
|
+
*/
|
|
16
|
+
type CredentialResponse = {
|
|
17
|
+
ErrorResponse?: {
|
|
18
|
+
Error?: {
|
|
19
|
+
Code?: string
|
|
20
|
+
Message?: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
AssumeRoleResponse: {
|
|
25
|
+
AssumeRoleResult: {
|
|
26
|
+
Credentials: {
|
|
27
|
+
AccessKeyId: string
|
|
28
|
+
SecretAccessKey: string
|
|
29
|
+
SessionToken: string
|
|
30
|
+
Expiration: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AssumeRoleProviderOptions {
|
|
37
|
+
stsEndpoint: string
|
|
38
|
+
accessKey: string
|
|
39
|
+
secretKey: string
|
|
40
|
+
durationSeconds?: number
|
|
41
|
+
sessionToken?: string
|
|
42
|
+
policy?: string
|
|
43
|
+
region?: string
|
|
44
|
+
roleArn?: string
|
|
45
|
+
roleSessionName?: string
|
|
46
|
+
externalId?: string
|
|
47
|
+
token?: string
|
|
48
|
+
webIdentityToken?: string
|
|
49
|
+
action?: string
|
|
50
|
+
transportAgent?: http.Agent
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const defaultExpirySeconds = 900
|
|
54
|
+
|
|
55
|
+
export class AssumeRoleProvider extends CredentialProvider {
|
|
56
|
+
private readonly stsEndpoint: URL
|
|
57
|
+
private readonly accessKey: string
|
|
58
|
+
private readonly secretKey: string
|
|
59
|
+
private readonly durationSeconds: number
|
|
60
|
+
private readonly policy?: string
|
|
61
|
+
private readonly region: string
|
|
62
|
+
private readonly roleArn?: string
|
|
63
|
+
private readonly roleSessionName?: string
|
|
64
|
+
private readonly externalId?: string
|
|
65
|
+
private readonly token?: string
|
|
66
|
+
private readonly webIdentityToken?: string
|
|
67
|
+
private readonly action: string
|
|
68
|
+
|
|
69
|
+
private _credentials: Credentials | null
|
|
70
|
+
private readonly expirySeconds: number
|
|
71
|
+
private accessExpiresAt = ''
|
|
72
|
+
private readonly transportAgent?: http.Agent
|
|
73
|
+
|
|
74
|
+
private readonly transport: Transport
|
|
75
|
+
|
|
76
|
+
constructor({
|
|
77
|
+
stsEndpoint,
|
|
78
|
+
accessKey,
|
|
79
|
+
secretKey,
|
|
80
|
+
durationSeconds = defaultExpirySeconds,
|
|
81
|
+
sessionToken,
|
|
82
|
+
policy,
|
|
83
|
+
region = '',
|
|
84
|
+
roleArn,
|
|
85
|
+
roleSessionName,
|
|
86
|
+
externalId,
|
|
87
|
+
token,
|
|
88
|
+
webIdentityToken,
|
|
89
|
+
action = 'AssumeRole',
|
|
90
|
+
transportAgent = undefined,
|
|
91
|
+
}: AssumeRoleProviderOptions) {
|
|
92
|
+
super({ accessKey, secretKey, sessionToken })
|
|
93
|
+
|
|
94
|
+
this.stsEndpoint = new URL(stsEndpoint)
|
|
95
|
+
this.accessKey = accessKey
|
|
96
|
+
this.secretKey = secretKey
|
|
97
|
+
this.policy = policy
|
|
98
|
+
this.region = region
|
|
99
|
+
this.roleArn = roleArn
|
|
100
|
+
this.roleSessionName = roleSessionName
|
|
101
|
+
this.externalId = externalId
|
|
102
|
+
this.token = token
|
|
103
|
+
this.webIdentityToken = webIdentityToken
|
|
104
|
+
this.action = action
|
|
105
|
+
|
|
106
|
+
this.durationSeconds = parseInt(durationSeconds as unknown as string)
|
|
107
|
+
|
|
108
|
+
let expirySeconds = this.durationSeconds
|
|
109
|
+
if (this.durationSeconds < defaultExpirySeconds) {
|
|
110
|
+
expirySeconds = defaultExpirySeconds
|
|
111
|
+
}
|
|
112
|
+
this.expirySeconds = expirySeconds // for calculating refresh of credentials.
|
|
113
|
+
|
|
114
|
+
// By default, nodejs uses a global agent if the 'agent' property
|
|
115
|
+
// is set to undefined. Otherwise, it's okay to assume the users
|
|
116
|
+
// know what they're doing if they specify a custom transport agent.
|
|
117
|
+
this.transportAgent = transportAgent
|
|
118
|
+
const isHttp: boolean = this.stsEndpoint.protocol === 'http:'
|
|
119
|
+
this.transport = isHttp ? http : https
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Internal Tracking variables
|
|
123
|
+
*/
|
|
124
|
+
this._credentials = null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getRequestConfig(): {
|
|
128
|
+
requestOptions: http.RequestOptions
|
|
129
|
+
requestData: string
|
|
130
|
+
} {
|
|
131
|
+
const hostValue = this.stsEndpoint.hostname
|
|
132
|
+
const portValue = this.stsEndpoint.port
|
|
133
|
+
const qryParams = new URLSearchParams({ Action: this.action, Version: '2011-06-15' })
|
|
134
|
+
|
|
135
|
+
qryParams.set('DurationSeconds', this.expirySeconds.toString())
|
|
136
|
+
|
|
137
|
+
if (this.policy) {
|
|
138
|
+
qryParams.set('Policy', this.policy)
|
|
139
|
+
}
|
|
140
|
+
if (this.roleArn) {
|
|
141
|
+
qryParams.set('RoleArn', this.roleArn)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this.roleSessionName != null) {
|
|
145
|
+
qryParams.set('RoleSessionName', this.roleSessionName)
|
|
146
|
+
}
|
|
147
|
+
if (this.token != null) {
|
|
148
|
+
qryParams.set('Token', this.token)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.webIdentityToken) {
|
|
152
|
+
qryParams.set('WebIdentityToken', this.webIdentityToken)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this.externalId) {
|
|
156
|
+
qryParams.set('ExternalId', this.externalId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const urlParams = qryParams.toString()
|
|
160
|
+
const contentSha256 = toSha256(urlParams)
|
|
161
|
+
|
|
162
|
+
const date = new Date()
|
|
163
|
+
|
|
164
|
+
const requestOptions = {
|
|
165
|
+
hostname: hostValue,
|
|
166
|
+
port: portValue,
|
|
167
|
+
path: '/',
|
|
168
|
+
protocol: this.stsEndpoint.protocol,
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
172
|
+
'content-length': urlParams.length.toString(),
|
|
173
|
+
host: hostValue,
|
|
174
|
+
'x-amz-date': makeDateLong(date),
|
|
175
|
+
'x-amz-content-sha256': contentSha256,
|
|
176
|
+
} as Record<string, string>,
|
|
177
|
+
agent: this.transportAgent,
|
|
178
|
+
} satisfies http.RequestOptions
|
|
179
|
+
|
|
180
|
+
requestOptions.headers.authorization = signV4ByServiceName(
|
|
181
|
+
requestOptions,
|
|
182
|
+
this.accessKey,
|
|
183
|
+
this.secretKey,
|
|
184
|
+
this.region,
|
|
185
|
+
date,
|
|
186
|
+
contentSha256,
|
|
187
|
+
'sts',
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
requestOptions,
|
|
192
|
+
requestData: urlParams,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async performRequest(): Promise<CredentialResponse> {
|
|
197
|
+
const { requestOptions, requestData } = this.getRequestConfig()
|
|
198
|
+
|
|
199
|
+
const res = await request(this.transport, requestOptions, requestData)
|
|
200
|
+
|
|
201
|
+
const body = await readAsString(res)
|
|
202
|
+
|
|
203
|
+
return parseXml(body)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
parseCredentials(respObj: CredentialResponse): Credentials {
|
|
207
|
+
if (respObj.ErrorResponse) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Unable to obtain credentials: ${respObj.ErrorResponse?.Error?.Code} ${respObj.ErrorResponse?.Error?.Message}`,
|
|
210
|
+
{ cause: respObj },
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const {
|
|
215
|
+
AssumeRoleResponse: {
|
|
216
|
+
AssumeRoleResult: {
|
|
217
|
+
Credentials: {
|
|
218
|
+
AccessKeyId: accessKey,
|
|
219
|
+
SecretAccessKey: secretKey,
|
|
220
|
+
SessionToken: sessionToken,
|
|
221
|
+
Expiration: expiresAt,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
} = respObj
|
|
226
|
+
|
|
227
|
+
this.accessExpiresAt = expiresAt
|
|
228
|
+
|
|
229
|
+
return new Credentials({ accessKey, secretKey, sessionToken })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async refreshCredentials(): Promise<Credentials> {
|
|
233
|
+
try {
|
|
234
|
+
const assumeRoleCredentials = await this.performRequest()
|
|
235
|
+
this._credentials = this.parseCredentials(assumeRoleCredentials)
|
|
236
|
+
} catch (err) {
|
|
237
|
+
throw new Error(`Failed to get Credentials: ${err}`, { cause: err })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return this._credentials
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async getCredentials(): Promise<Credentials> {
|
|
244
|
+
if (this._credentials && !this.isAboutToExpire()) {
|
|
245
|
+
return this._credentials
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._credentials = await this.refreshCredentials()
|
|
249
|
+
return this._credentials
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
isAboutToExpire() {
|
|
253
|
+
const expiresAt = new Date(this.accessExpiresAt)
|
|
254
|
+
const provisionalExpiry = new Date(Date.now() + 1000 * 10) // check before 10 seconds.
|
|
255
|
+
return provisionalExpiry > expiresAt
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// deprecated default export, please use named exports.
|
|
260
|
+
// keep for backward compatibility.
|
|
261
|
+
// eslint-disable-next-line import/no-default-export
|
|
262
|
+
export default AssumeRoleProvider
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Credentials } from './Credentials.ts'
|
|
2
|
+
|
|
3
|
+
export class CredentialProvider {
|
|
4
|
+
private credentials: Credentials
|
|
5
|
+
|
|
6
|
+
constructor({ accessKey, secretKey, sessionToken }: { accessKey: string; secretKey: string; sessionToken?: string }) {
|
|
7
|
+
this.credentials = new Credentials({
|
|
8
|
+
accessKey,
|
|
9
|
+
secretKey,
|
|
10
|
+
sessionToken,
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getCredentials(): Promise<Credentials> {
|
|
15
|
+
return this.credentials.get()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setCredentials(credentials: Credentials) {
|
|
19
|
+
if (credentials instanceof Credentials) {
|
|
20
|
+
this.credentials = credentials
|
|
21
|
+
} else {
|
|
22
|
+
throw new Error('Unable to set Credentials. it should be an instance of Credentials class')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setAccessKey(accessKey: string) {
|
|
27
|
+
this.credentials.setAccessKey(accessKey)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getAccessKey() {
|
|
31
|
+
return this.credentials.getAccessKey()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setSecretKey(secretKey: string) {
|
|
35
|
+
this.credentials.setSecretKey(secretKey)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSecretKey() {
|
|
39
|
+
return this.credentials.getSecretKey()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setSessionToken(sessionToken: string) {
|
|
43
|
+
this.credentials.setSessionToken(sessionToken)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getSessionToken() {
|
|
47
|
+
return this.credentials.getSessionToken()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// deprecated default export, please use named exports.
|
|
52
|
+
// keep for backward compatibility.
|
|
53
|
+
// eslint-disable-next-line import/no-default-export
|
|
54
|
+
export default CredentialProvider
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class Credentials {
|
|
2
|
+
public accessKey: string
|
|
3
|
+
public secretKey: string
|
|
4
|
+
public sessionToken?: string
|
|
5
|
+
|
|
6
|
+
constructor({ accessKey, secretKey, sessionToken }: { accessKey: string; secretKey: string; sessionToken?: string }) {
|
|
7
|
+
this.accessKey = accessKey
|
|
8
|
+
this.secretKey = secretKey
|
|
9
|
+
this.sessionToken = sessionToken
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setAccessKey(accessKey: string) {
|
|
13
|
+
this.accessKey = accessKey
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getAccessKey() {
|
|
17
|
+
return this.accessKey
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setSecretKey(secretKey: string) {
|
|
21
|
+
this.secretKey = secretKey
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getSecretKey() {
|
|
25
|
+
return this.secretKey
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setSessionToken(sessionToken: string) {
|
|
29
|
+
this.sessionToken = sessionToken
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getSessionToken() {
|
|
33
|
+
return this.sessionToken
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get(): Credentials {
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// deprecated default export, please use named exports.
|
|
42
|
+
// keep for backward compatibility.
|
|
43
|
+
// eslint-disable-next-line import/no-default-export
|
|
44
|
+
export default Credentials
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
|
+
import * as http from 'node:http'
|
|
3
|
+
import * as https from 'node:https'
|
|
4
|
+
import { URL, URLSearchParams } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { CredentialProvider } from './CredentialProvider.ts'
|
|
7
|
+
import { Credentials } from './Credentials.ts'
|
|
8
|
+
import { parseXml } from './internal/helper.ts'
|
|
9
|
+
import { request } from './internal/request.ts'
|
|
10
|
+
import { readAsString } from './internal/response.ts'
|
|
11
|
+
|
|
12
|
+
interface AssumeRoleResponse {
|
|
13
|
+
AssumeRoleWithWebIdentityResponse: {
|
|
14
|
+
AssumeRoleWithWebIdentityResult: {
|
|
15
|
+
Credentials: {
|
|
16
|
+
AccessKeyId: string
|
|
17
|
+
SecretAccessKey: string
|
|
18
|
+
SessionToken: string
|
|
19
|
+
Expiration: string
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface EcsCredentials {
|
|
26
|
+
AccessKeyID: string
|
|
27
|
+
SecretAccessKey: string
|
|
28
|
+
Token: string
|
|
29
|
+
Expiration: string
|
|
30
|
+
Code: string
|
|
31
|
+
Message: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IamAwsProviderOptions {
|
|
35
|
+
customEndpoint?: string
|
|
36
|
+
transportAgent?: http.Agent
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class IamAwsProvider extends CredentialProvider {
|
|
40
|
+
private readonly customEndpoint?: string
|
|
41
|
+
|
|
42
|
+
private _credentials: Credentials | null
|
|
43
|
+
private readonly transportAgent?: http.Agent
|
|
44
|
+
private accessExpiresAt = ''
|
|
45
|
+
|
|
46
|
+
constructor({ customEndpoint = undefined, transportAgent = undefined }: IamAwsProviderOptions) {
|
|
47
|
+
super({ accessKey: '', secretKey: '' })
|
|
48
|
+
|
|
49
|
+
this.customEndpoint = customEndpoint
|
|
50
|
+
this.transportAgent = transportAgent
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal Tracking variables
|
|
54
|
+
*/
|
|
55
|
+
this._credentials = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getCredentials(): Promise<Credentials> {
|
|
59
|
+
if (!this._credentials || this.isAboutToExpire()) {
|
|
60
|
+
this._credentials = await this.fetchCredentials()
|
|
61
|
+
}
|
|
62
|
+
return this._credentials
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async fetchCredentials(): Promise<Credentials> {
|
|
66
|
+
try {
|
|
67
|
+
// check for IRSA (https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
|
|
68
|
+
const tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE
|
|
69
|
+
if (tokenFile) {
|
|
70
|
+
return await this.fetchCredentialsUsingTokenFile(tokenFile)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// try with IAM role for EC2 instances (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
|
|
74
|
+
let tokenHeader = 'Authorization'
|
|
75
|
+
let token = process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN
|
|
76
|
+
const relativeUri = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
|
|
77
|
+
const fullUri = process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI
|
|
78
|
+
let url: URL
|
|
79
|
+
if (relativeUri) {
|
|
80
|
+
url = new URL(relativeUri, 'http://169.254.170.2')
|
|
81
|
+
} else if (fullUri) {
|
|
82
|
+
url = new URL(fullUri)
|
|
83
|
+
} else {
|
|
84
|
+
token = await this.fetchImdsToken()
|
|
85
|
+
tokenHeader = 'X-aws-ec2-metadata-token'
|
|
86
|
+
url = await this.getIamRoleNamedUrl(token)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this.requestCredentials(url, tokenHeader, token)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new Error(`Failed to get Credentials: ${err}`, { cause: err })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async fetchCredentialsUsingTokenFile(tokenFile: string): Promise<Credentials> {
|
|
96
|
+
const token = await fs.readFile(tokenFile, { encoding: 'utf8' })
|
|
97
|
+
const region = process.env.AWS_REGION
|
|
98
|
+
const stsEndpoint = new URL(region ? `https://sts.${region}.amazonaws.com` : 'https://sts.amazonaws.com')
|
|
99
|
+
|
|
100
|
+
const hostValue = stsEndpoint.hostname
|
|
101
|
+
const portValue = stsEndpoint.port
|
|
102
|
+
const qryParams = new URLSearchParams({
|
|
103
|
+
Action: 'AssumeRoleWithWebIdentity',
|
|
104
|
+
Version: '2011-06-15',
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const roleArn = process.env.AWS_ROLE_ARN
|
|
108
|
+
if (roleArn) {
|
|
109
|
+
qryParams.set('RoleArn', roleArn)
|
|
110
|
+
const roleSessionName = process.env.AWS_ROLE_SESSION_NAME
|
|
111
|
+
qryParams.set('RoleSessionName', roleSessionName ? roleSessionName : Date.now().toString())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
qryParams.set('WebIdentityToken', token)
|
|
115
|
+
qryParams.sort()
|
|
116
|
+
|
|
117
|
+
const requestOptions = {
|
|
118
|
+
hostname: hostValue,
|
|
119
|
+
port: portValue,
|
|
120
|
+
path: `${stsEndpoint.pathname}?${qryParams.toString()}`,
|
|
121
|
+
protocol: stsEndpoint.protocol,
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {},
|
|
124
|
+
agent: this.transportAgent,
|
|
125
|
+
} satisfies http.RequestOptions
|
|
126
|
+
|
|
127
|
+
const transport = stsEndpoint.protocol === 'http:' ? http : https
|
|
128
|
+
const res = await request(transport, requestOptions, null)
|
|
129
|
+
const body = await readAsString(res)
|
|
130
|
+
|
|
131
|
+
const assumeRoleResponse: AssumeRoleResponse = parseXml(body)
|
|
132
|
+
const creds = assumeRoleResponse.AssumeRoleWithWebIdentityResponse.AssumeRoleWithWebIdentityResult.Credentials
|
|
133
|
+
this.accessExpiresAt = creds.Expiration
|
|
134
|
+
return new Credentials({
|
|
135
|
+
accessKey: creds.AccessKeyId,
|
|
136
|
+
secretKey: creds.SecretAccessKey,
|
|
137
|
+
sessionToken: creds.SessionToken,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async fetchImdsToken() {
|
|
142
|
+
const endpoint = this.customEndpoint ? this.customEndpoint : 'http://169.254.169.254'
|
|
143
|
+
const url = new URL('/latest/api/token', endpoint)
|
|
144
|
+
|
|
145
|
+
const requestOptions = {
|
|
146
|
+
hostname: url.hostname,
|
|
147
|
+
port: url.port,
|
|
148
|
+
path: `${url.pathname}${url.search}`,
|
|
149
|
+
protocol: url.protocol,
|
|
150
|
+
method: 'PUT',
|
|
151
|
+
headers: {
|
|
152
|
+
'X-aws-ec2-metadata-token-ttl-seconds': '21600',
|
|
153
|
+
},
|
|
154
|
+
agent: this.transportAgent,
|
|
155
|
+
} satisfies http.RequestOptions
|
|
156
|
+
|
|
157
|
+
const transport = url.protocol === 'http:' ? http : https
|
|
158
|
+
const res = await request(transport, requestOptions, null)
|
|
159
|
+
return await readAsString(res)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async getIamRoleNamedUrl(token: string) {
|
|
163
|
+
const endpoint = this.customEndpoint ? this.customEndpoint : 'http://169.254.169.254'
|
|
164
|
+
const url = new URL('latest/meta-data/iam/security-credentials/', endpoint)
|
|
165
|
+
|
|
166
|
+
const roleName = await this.getIamRoleName(url, token)
|
|
167
|
+
return new URL(`${url.pathname}/${encodeURIComponent(roleName)}`, url.origin)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async getIamRoleName(url: URL, token: string): Promise<string> {
|
|
171
|
+
const requestOptions = {
|
|
172
|
+
hostname: url.hostname,
|
|
173
|
+
port: url.port,
|
|
174
|
+
path: `${url.pathname}${url.search}`,
|
|
175
|
+
protocol: url.protocol,
|
|
176
|
+
method: 'GET',
|
|
177
|
+
headers: {
|
|
178
|
+
'X-aws-ec2-metadata-token': token,
|
|
179
|
+
},
|
|
180
|
+
agent: this.transportAgent,
|
|
181
|
+
} satisfies http.RequestOptions
|
|
182
|
+
|
|
183
|
+
const transport = url.protocol === 'http:' ? http : https
|
|
184
|
+
const res = await request(transport, requestOptions, null)
|
|
185
|
+
const body = await readAsString(res)
|
|
186
|
+
const roleNames = body.split(/\r\n|[\n\r\u2028\u2029]/)
|
|
187
|
+
if (roleNames.length === 0) {
|
|
188
|
+
throw new Error(`No IAM roles attached to EC2 service ${url}`)
|
|
189
|
+
}
|
|
190
|
+
return roleNames[0] as string
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async requestCredentials(url: URL, tokenHeader: string, token: string | undefined): Promise<Credentials> {
|
|
194
|
+
const headers: Record<string, string> = {}
|
|
195
|
+
if (token) {
|
|
196
|
+
headers[tokenHeader] = token
|
|
197
|
+
}
|
|
198
|
+
const requestOptions = {
|
|
199
|
+
hostname: url.hostname,
|
|
200
|
+
port: url.port,
|
|
201
|
+
path: `${url.pathname}${url.search}`,
|
|
202
|
+
protocol: url.protocol,
|
|
203
|
+
method: 'GET',
|
|
204
|
+
headers: headers,
|
|
205
|
+
agent: this.transportAgent,
|
|
206
|
+
} satisfies http.RequestOptions
|
|
207
|
+
|
|
208
|
+
const transport = url.protocol === 'http:' ? http : https
|
|
209
|
+
const res = await request(transport, requestOptions, null)
|
|
210
|
+
const body = await readAsString(res)
|
|
211
|
+
const ecsCredentials = JSON.parse(body) as EcsCredentials
|
|
212
|
+
if (!ecsCredentials.Code || ecsCredentials.Code != 'Success') {
|
|
213
|
+
throw new Error(`${url} failed with code ${ecsCredentials.Code} and message ${ecsCredentials.Message}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.accessExpiresAt = ecsCredentials.Expiration
|
|
217
|
+
return new Credentials({
|
|
218
|
+
accessKey: ecsCredentials.AccessKeyID,
|
|
219
|
+
secretKey: ecsCredentials.SecretAccessKey,
|
|
220
|
+
sessionToken: ecsCredentials.Token,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private isAboutToExpire() {
|
|
225
|
+
const expiresAt = new Date(this.accessExpiresAt)
|
|
226
|
+
const provisionalExpiry = new Date(Date.now() + 1000 * 10) // 10 seconds leeway
|
|
227
|
+
return provisionalExpiry > expiresAt
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// deprecated default export, please use named exports.
|
|
232
|
+
// keep for backward compatibility.
|
|
233
|
+
// eslint-disable-next-line import/no-default-export
|
|
234
|
+
export default IamAwsProvider
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hanzo S3 Javascript Library for Amazon S3 Compatible Cloud Storage, (C) 2015 Hanzo AI, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/// <reference lib="ES2022.Error" />
|
|
18
|
+
|
|
19
|
+
class ExtendableError extends Error {
|
|
20
|
+
constructor(message?: string, opt?: ErrorOptions) {
|
|
21
|
+
// error Option {cause?: unknown} is a 'nice to have',
|
|
22
|
+
// don't use it internally
|
|
23
|
+
super(message, opt)
|
|
24
|
+
// set error name, otherwise it's always 'Error'
|
|
25
|
+
this.name = this.constructor.name
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* AnonymousRequestError is generated for anonymous keys on specific
|
|
31
|
+
* APIs. NOTE: PresignedURL generation always requires access keys.
|
|
32
|
+
*/
|
|
33
|
+
export class AnonymousRequestError extends ExtendableError {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* InvalidArgumentError is generated for all invalid arguments.
|
|
37
|
+
*/
|
|
38
|
+
export class InvalidArgumentError extends ExtendableError {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* InvalidPortError is generated when a non integer value is provided
|
|
42
|
+
* for ports.
|
|
43
|
+
*/
|
|
44
|
+
export class InvalidPortError extends ExtendableError {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* InvalidEndpointError is generated when an invalid end point value is
|
|
48
|
+
* provided which does not follow domain standards.
|
|
49
|
+
*/
|
|
50
|
+
export class InvalidEndpointError extends ExtendableError {}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* InvalidBucketNameError is generated when an invalid bucket name is
|
|
54
|
+
* provided which does not follow AWS S3 specifications.
|
|
55
|
+
* http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
|
|
56
|
+
*/
|
|
57
|
+
export class InvalidBucketNameError extends ExtendableError {}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* InvalidObjectNameError is generated when an invalid object name is
|
|
61
|
+
* provided which does not follow AWS S3 specifications.
|
|
62
|
+
* http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
|
|
63
|
+
*/
|
|
64
|
+
export class InvalidObjectNameError extends ExtendableError {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* AccessKeyRequiredError generated by signature methods when access
|
|
68
|
+
* key is not found.
|
|
69
|
+
*/
|
|
70
|
+
export class AccessKeyRequiredError extends ExtendableError {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* SecretKeyRequiredError generated by signature methods when secret
|
|
74
|
+
* key is not found.
|
|
75
|
+
*/
|
|
76
|
+
export class SecretKeyRequiredError extends ExtendableError {}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ExpiresParamError generated when expires parameter value is not
|
|
80
|
+
* well within stipulated limits.
|
|
81
|
+
*/
|
|
82
|
+
export class ExpiresParamError extends ExtendableError {}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* InvalidDateError generated when invalid date is found.
|
|
86
|
+
*/
|
|
87
|
+
export class InvalidDateError extends ExtendableError {}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* InvalidPrefixError generated when object prefix provided is invalid
|
|
91
|
+
* or does not conform to AWS S3 object key restrictions.
|
|
92
|
+
*/
|
|
93
|
+
export class InvalidPrefixError extends ExtendableError {}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* InvalidBucketPolicyError generated when the given bucket policy is invalid.
|
|
97
|
+
*/
|
|
98
|
+
export class InvalidBucketPolicyError extends ExtendableError {}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* IncorrectSizeError generated when total data read mismatches with
|
|
102
|
+
* the input size.
|
|
103
|
+
*/
|
|
104
|
+
export class IncorrectSizeError extends ExtendableError {}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* InvalidXMLError generated when an unknown XML is found.
|
|
108
|
+
*/
|
|
109
|
+
export class InvalidXMLError extends ExtendableError {}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* S3Error is generated for errors returned from S3 server.
|
|
113
|
+
* see getErrorTransformer for details
|
|
114
|
+
*/
|
|
115
|
+
export class S3Error extends ExtendableError {
|
|
116
|
+
code?: string
|
|
117
|
+
region?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class IsValidBucketNameError extends ExtendableError {}
|