sigflint 1.0.0 → 1.0.2

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/src/base.js CHANGED
@@ -9,20 +9,36 @@
9
9
 
10
10
  export const emptyBodyHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
11
11
 
12
- /** The scheme of a signature: its algorithm name and how the credential scope is written. SigV4a (v4a.js) has its own. */
13
- export const symmetric = { algorithm: 'AWS4-HMAC-SHA256', scope: (date, region, service) => `${date}/${region}/${service}/aws4_request` };
12
+ /**
13
+ * @typedef {Object} Scheme
14
+ * @property {string} algorithm
15
+ * @property {(date: string, region: string, service: string) => string} scope
16
+ */
17
+
18
+ /**
19
+ * The scheme of a signature: its algorithm name and how the credential scope is written. SigV4a (v4a.js) has its own.
20
+ * @type {Scheme}
21
+ */
22
+ export const symmetric = {
23
+ algorithm: 'AWS4-HMAC-SHA256',
24
+ scope: (date, region, service) => `${date}/${region}/${service}/aws4_request`,
25
+ };
14
26
 
15
27
  const reSafePath = /^[A-Za-z0-9\-._~/]*$/;
16
28
  const reSafeQuery = /^[A-Za-z0-9\-._~=&]*$/;
17
29
  const reSpaces = /[ \t]+/g;
18
- const reDirty = /\t| /;
30
+ const reDirty = /\t| {2}/;
19
31
 
20
32
  // ASCII code -> what it is written as in a canonical path / query. url.pathname and url.search are always ASCII (the URL parser percent-encodes everything else).
21
33
  // Built on first use, so importing this module costs nothing.
34
+ /** @type {string[] | undefined} */
22
35
  let pathTable;
36
+ /** @type {string[] | undefined} */
23
37
  let queryTable;
24
38
  function tables() {
25
- if (pathTable !== undefined) return;
39
+ if (pathTable !== undefined) {
40
+ return;
41
+ }
26
42
  pathTable = new Array(256);
27
43
  queryTable = new Array(256);
28
44
  for (let b = 0; b < 256; b++) {
@@ -34,22 +50,35 @@ function tables() {
34
50
  }
35
51
  }
36
52
 
53
+ /** @param {number} code */
37
54
  function hexValue(code) {
38
- if (code >= 48 && code <= 57) return code - 48; // 0-9
39
- if (code >= 65 && code <= 70) return code - 55; // A-F
40
- if (code >= 97 && code <= 102) return code - 87; // a-f
55
+ if (code >= 48 && code <= 57) {
56
+ return code - 48;
57
+ } // 0-9
58
+ if (code >= 65 && code <= 70) {
59
+ return code - 55;
60
+ } // A-F
61
+ if (code >= 97 && code <= 102) {
62
+ return code - 87;
63
+ } // a-f
41
64
  return -1;
42
65
  }
43
66
 
44
67
  // Decodes %XX escapes to bytes (a '%' that does not start an escape is the byte 0x25) and encodes each byte with the table; '+' is a space.
45
68
  // Runs of characters that are already canonical are copied as one piece.
69
+ /**
70
+ * @param {string} string
71
+ * @param {string[]} table
72
+ */
46
73
  function canonicalEncode(string, table) {
47
74
  let out = '';
48
75
  let start = 0;
49
76
  for (let i = 0; i < string.length; i++) {
50
77
  const code = string.charCodeAt(i);
51
- const same = table[code];
52
- if (same.length === 1 && code !== 43) continue;
78
+ const same = /** @type {string} */ (table[code]);
79
+ if (same.length === 1 && code !== 43) {
80
+ continue;
81
+ }
53
82
  out += string.slice(start, i);
54
83
  start = i + 1;
55
84
  if (code === 37) {
@@ -69,39 +98,69 @@ function canonicalEncode(string, table) {
69
98
  return start === 0 ? string : out + string.slice(start);
70
99
  }
71
100
 
101
+ /** @param {string} pathname */
72
102
  function canonicalPathGeneral(pathname) {
73
103
  const parts = pathname.split('/');
74
104
  const kept = [];
75
105
  for (let i = 0; i < parts.length; i++) {
76
106
  const part = parts[i];
77
- if (part === '' || part === '.') continue;
78
- if (part === '..') kept.pop();
79
- else kept.push(part);
107
+ if (part === '' || part === '.') {
108
+ continue;
109
+ }
110
+ if (part === '..') {
111
+ kept.pop();
112
+ } else {
113
+ kept.push(part);
114
+ }
115
+ }
116
+ const path =
117
+ (pathname.startsWith('/') ? '/' : '') +
118
+ kept.join('/') +
119
+ (pathname.endsWith('/') && kept.length > 0 ? '/' : '');
120
+ if (reSafePath.test(path)) {
121
+ return path === '' ? '/' : path;
80
122
  }
81
- const path = (pathname.startsWith('/') ? '/' : '') + kept.join('/') + (pathname.endsWith('/') && kept.length > 0 ? '/' : '');
82
- if (reSafePath.test(path)) return path === '' ? '/' : path;
83
123
  tables();
84
124
  let out = '';
85
- for (let i = 0; i < path.length; i++) out += queryTable[path.charCodeAt(i)] === path[i] || path[i] === '/' ? path[i] : queryTable[path.charCodeAt(i)];
125
+ for (let i = 0; i < path.length; i++) {
126
+ out +=
127
+ /** @type {string[]} */ (queryTable)[path.charCodeAt(i)] === path[i] || path[i] === '/'
128
+ ? path[i]
129
+ : /** @type {string[]} */ (queryTable)[path.charCodeAt(i)];
130
+ }
86
131
  return out;
87
132
  }
88
133
 
134
+ /** @param {string} pathname */
89
135
  function canonicalPathS3(pathname) {
90
- if (reSafePath.test(pathname)) return pathname === '' ? '/' : pathname;
136
+ if (reSafePath.test(pathname)) {
137
+ return pathname === '' ? '/' : pathname;
138
+ }
91
139
  tables();
92
- return canonicalEncode(pathname, pathTable);
140
+ return canonicalEncode(pathname, /** @type {string[]} */ (pathTable));
93
141
  }
94
142
 
95
- /** The canonical path for a service. */
96
- export const canonicalPath = (service, pathname) => (service === 's3' ? canonicalPathS3(pathname) : canonicalPathGeneral(pathname));
143
+ /**
144
+ * The canonical path for a service.
145
+ * @param {string} service
146
+ * @param {string} pathname
147
+ */
148
+ export const canonicalPath = (service, pathname) =>
149
+ service === 's3' ? canonicalPathS3(pathname) : canonicalPathGeneral(pathname);
97
150
 
151
+ /**
152
+ * @param {[string, string]} a
153
+ * @param {[string, string]} b
154
+ */
98
155
  function comparePairs(a, b) {
99
156
  return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
100
157
  }
101
158
 
102
- /** search is url.search, which is '' or starts with '?' */
159
+ /** @param {string} search - url.search, which is '' or starts with '?' */
103
160
  export function canonicalQuery(search) {
104
- if (search.length < 2) return '';
161
+ if (search.length < 2) {
162
+ return '';
163
+ }
105
164
  const query = search.slice(1);
106
165
  const simple = reSafeQuery.test(query);
107
166
  if (simple) {
@@ -113,46 +172,85 @@ export function canonicalQuery(search) {
113
172
  let start = 0;
114
173
  for (;;) {
115
174
  let end = query.indexOf('&', start);
116
- if (end === -1) end = query.length;
175
+ if (end === -1) {
176
+ end = query.length;
177
+ }
117
178
  let eq = query.indexOf('=', start);
118
- if (eq === -1 || eq > end) eq = end;
179
+ if (eq === -1 || eq > end) {
180
+ eq = end;
181
+ }
119
182
  const name = query.slice(start, eq);
120
183
  const value = query.slice(eq + 1, end);
121
- if (end === start || eq === start || value.includes('=') || (start > 0 && (name < previousName || (name === previousName && value <= previousValue)))) break;
184
+ if (
185
+ end === start ||
186
+ eq === start ||
187
+ value.includes('=') ||
188
+ (start > 0 && (name < previousName || (name === previousName && value <= previousValue)))
189
+ ) {
190
+ break;
191
+ }
122
192
  out += (start === 0 ? '' : '&') + name + '=' + value;
123
- if (end === query.length) return out;
193
+ if (end === query.length) {
194
+ return out;
195
+ }
124
196
  previousName = name;
125
197
  previousValue = value;
126
198
  start = end + 1;
127
199
  }
128
200
  }
129
201
  tables();
202
+ /** @type {[string, string][]} */
130
203
  const pairs = [];
131
204
  for (const part of query.split('&')) {
132
- if (part === '') continue;
205
+ if (part === '') {
206
+ continue;
207
+ }
133
208
  const eq = part.indexOf('=');
134
209
  const rawName = eq === -1 ? part : part.slice(0, eq);
135
210
  const rawValue = eq === -1 ? '' : part.slice(eq + 1);
136
211
  // a '=' inside a value must be encoded, so the plain-characters shortcut applies only to values without one
137
- pairs.push(simple && !rawValue.includes('=') ? [rawName, rawValue] : [canonicalEncode(rawName, queryTable), canonicalEncode(rawValue, queryTable)]);
212
+ pairs.push(
213
+ simple && !rawValue.includes('=')
214
+ ? [rawName, rawValue]
215
+ : [
216
+ canonicalEncode(rawName, /** @type {string[]} */ (queryTable)),
217
+ canonicalEncode(rawValue, /** @type {string[]} */ (queryTable)),
218
+ ],
219
+ );
220
+ }
221
+ if (pairs.length > 1) {
222
+ pairs.sort(comparePairs);
138
223
  }
139
- if (pairs.length > 1) pairs.sort(comparePairs);
140
224
  let out = '';
141
- for (let i = 0; i < pairs.length; i++) out += (i === 0 ? '' : '&') + pairs[i][0] + '=' + pairs[i][1];
225
+ for (let i = 0; i < pairs.length; i++) {
226
+ out +=
227
+ (i === 0 ? '' : '&') +
228
+ /** @type {[string, string]} */ (pairs[i])[0] +
229
+ '=' +
230
+ /** @type {[string, string]} */ (pairs[i])[1];
231
+ }
142
232
  return out;
143
233
  }
144
234
 
145
- /** inserts into the name-ordered lists, replacing an entry with the same name */
235
+ /**
236
+ * Inserts into the name-ordered lists, replacing an entry with the same name.
237
+ * @param {string[]} names
238
+ * @param {string[]} values
239
+ * @param {string} name
240
+ * @param {string} value
241
+ */
146
242
  export function insert(names, values, name, value) {
147
243
  let i = names.length;
148
- while (i > 0 && names[i - 1] > name) i--;
244
+ while (i > 0 && /** @type {string} */ (names[i - 1]) > name) {
245
+ i--;
246
+ }
149
247
  if (i > 0 && names[i - 1] === name) {
150
248
  values[i - 1] = value;
151
249
  return;
152
250
  }
153
251
  for (let j = names.length; j > i; j--) {
154
- names[j] = names[j - 1];
155
- values[j] = values[j - 1];
252
+ names[j] = /** @type {string} */ (names[j - 1]);
253
+ values[j] = /** @type {string} */ (values[j - 1]);
156
254
  }
157
255
  names[i] = name;
158
256
  values[i] = value;
@@ -161,15 +259,24 @@ export function insert(names, values, name, value) {
161
259
  /**
162
260
  * Refuses a signHeaders entry that cannot be signed as given: x-amz-date and authorization are set by the signer, `__proto__` cannot be a property of the
163
261
  * returned headers, and a line break in a value would start another canonical header line. Returns the lowercase name.
262
+ * @param {string} name
263
+ * @param {string} value
164
264
  */
165
265
  export function signHeaderName(name, value) {
166
266
  const lowerName = name.toLowerCase();
167
- if (lowerName === 'x-amz-date' || lowerName === 'authorization' || lowerName === '__proto__') throw new TypeError(`signHeaders cannot contain ${lowerName}`);
168
- if (/[\r\n]/.test(value)) throw new TypeError(`signHeaders ${lowerName} has a line break in its value`);
267
+ if (lowerName === 'x-amz-date' || lowerName === 'authorization' || lowerName === '__proto__') {
268
+ throw new TypeError(`signHeaders cannot contain ${lowerName}`);
269
+ }
270
+ if (/[\r\n]/.test(value)) {
271
+ throw new TypeError(`signHeaders ${lowerName} has a line break in its value`);
272
+ }
169
273
  return lowerName;
170
274
  }
171
275
 
172
- /** A header value as it is signed: trimmed, runs of spaces and tabs collapsed. */
276
+ /**
277
+ * A header value as it is signed: trimmed, runs of spaces and tabs collapsed.
278
+ * @param {unknown} value
279
+ */
173
280
  export const headerValue = (value) => {
174
281
  const trimmed = String(value).trim();
175
282
  return reDirty.test(trimmed) ? trimmed.replace(reSpaces, ' ') : trimmed;
@@ -178,11 +285,20 @@ export const headerValue = (value) => {
178
285
  // 20130524T000000Z, recomputed only when the second changes
179
286
  let lastSecond = NaN;
180
287
  let lastDatetime = '';
288
+ /** @param {number} now - milliseconds since the epoch */
181
289
  export function amzDatetime(now) {
182
290
  const second = Math.floor(now / 1000);
183
291
  if (second !== lastSecond) {
184
292
  const iso = new Date(second * 1000).toISOString(); // 2013-05-24T00:00:00.000Z
185
- lastDatetime = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10) + 'T' + iso.slice(11, 13) + iso.slice(14, 16) + iso.slice(17, 19) + 'Z';
293
+ lastDatetime =
294
+ iso.slice(0, 4) +
295
+ iso.slice(5, 7) +
296
+ iso.slice(8, 10) +
297
+ 'T' +
298
+ iso.slice(11, 13) +
299
+ iso.slice(14, 16) +
300
+ iso.slice(17, 19) +
301
+ 'Z';
186
302
  lastSecond = second;
187
303
  }
188
304
  return lastDatetime;
@@ -190,7 +306,7 @@ export function amzDatetime(now) {
190
306
 
191
307
  /**
192
308
  * @typedef {Object} SignRequest
193
- * @property {string} method - GET, HEAD, PUT, POST, DELETE
309
+ * @property {string} method - GET, HEAD, PUT, POST, DELETE; any case, signed in upper case (as botocore does)
194
310
  * @property {URL} url
195
311
  * @property {string} [service] - default 's3'
196
312
  * @property {string} [region] - for example us-west-2
@@ -219,15 +335,32 @@ export function amzDatetime(now) {
219
335
  * @property {ReturnType<typeof createKeys>} getSigningKey - the entry's one signing-key cache, shared by every symmetric signer
220
336
  */
221
337
 
222
- /** The payload hash for x-amz-content-sha256. The hash function may return a promise, which is then the result. */
338
+ /**
339
+ * The payload hash for x-amz-content-sha256. The hash function may return a promise, which is then the result.
340
+ * @template {string | Promise<string>} H
341
+ * @param {SignRequest} request
342
+ * @param {(data: string) => H} hexHash
343
+ * @returns {string | H}
344
+ */
223
345
  export function payloadHashOf(request, hexHash) {
224
346
  const body = request.body ?? '';
225
- return request.unsignedPayload ? 'UNSIGNED-PAYLOAD' : body.length === 0 ? emptyBodyHash : hexHash(body);
347
+ return request.unsignedPayload
348
+ ? 'UNSIGNED-PAYLOAD'
349
+ : body.length === 0
350
+ ? emptyBodyHash
351
+ : hexHash(body);
226
352
  }
227
353
 
228
- /** Everything up to the hashing: the headers to return, the canonical request and the credential scope. */
354
+ /**
355
+ * Everything up to the hashing: the headers to return, the canonical request and the credential scope.
356
+ * @param {SignRequest} request
357
+ * @param {Credentials} credentials
358
+ * @param {string} payloadHash
359
+ * @param {Scheme} [scheme]
360
+ */
229
361
  export function prepare(request, credentials, payloadHash, scheme = symmetric) {
230
- const { method, url, signHeaders } = request;
362
+ const { url, signHeaders } = request;
363
+ const method = request.method.toUpperCase(); // 'get' is signed as GET, which is what fetch sends and what botocore signs
231
364
  const region = request.region ?? '';
232
365
  const service = request.service ?? 's3';
233
366
  // S3 always sends x-amz-content-sha256; other services only when the payload is unsigned
@@ -236,31 +369,42 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
236
369
  const datetime = amzDatetime(request.now ?? Date.now());
237
370
  const date = datetime.slice(0, 8);
238
371
 
239
- const headers = withHash ? { 'x-amz-date': datetime, 'x-amz-content-sha256': payloadHash } : { 'x-amz-date': datetime }; // as returned
372
+ /** @type {AuthHeaders} */
373
+ const headers = withHash
374
+ ? { 'x-amz-date': datetime, 'x-amz-content-sha256': payloadHash }
375
+ : { 'x-amz-date': datetime }; // as returned
240
376
 
241
377
  const extraNames = signHeaders === undefined ? [] : Object.keys(signHeaders);
242
378
  let canonicalHeaders;
243
379
  let signedHeaders;
244
380
  if (extraNames.length === 0 && !sessionToken) {
245
381
  // the usual request: only the headers that are always signed, already in sorted order
246
- canonicalHeaders = withHash ? `host:${url.host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${datetime}\n` : `host:${url.host}\nx-amz-date:${datetime}\n`;
382
+ canonicalHeaders = withHash
383
+ ? `host:${url.host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${datetime}\n`
384
+ : `host:${url.host}\nx-amz-date:${datetime}\n`;
247
385
  signedHeaders = withHash ? 'host;x-amz-content-sha256;x-amz-date' : 'host;x-amz-date';
248
386
  } else {
249
387
  // the extra headers (and the session token) in name order, then merged with the ones that are always signed;
250
388
  // a header named like one of those, or like an earlier extra, replaces it
389
+ /** @type {string[]} */
251
390
  const names = [];
391
+ /** @type {string[]} */
252
392
  const values = [];
253
393
  if (sessionToken) {
254
394
  headers['x-amz-security-token'] = sessionToken;
255
395
  insert(names, values, 'x-amz-security-token', sessionToken.trim());
256
396
  }
257
397
  for (const name of extraNames) {
258
- const value = /** @type {Record<string, string>} */ (signHeaders)[name];
398
+ const value = /** @type {string} */ (
399
+ /** @type {Record<string, string>} */ (signHeaders)[name]
400
+ );
259
401
  const lowerName = signHeaderName(name, value);
260
402
  headers[lowerName] = value;
261
403
  insert(names, values, lowerName, headerValue(value));
262
404
  }
263
- const baseNames = withHash ? ['host', 'x-amz-content-sha256', 'x-amz-date'] : ['host', 'x-amz-date'];
405
+ const baseNames = withHash
406
+ ? ['host', 'x-amz-content-sha256', 'x-amz-date']
407
+ : ['host', 'x-amz-date'];
264
408
  const baseValues = withHash ? [url.host, payloadHash, datetime] : [url.host, datetime];
265
409
  const nBase = baseNames.length;
266
410
  canonicalHeaders = '';
@@ -270,50 +414,102 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
270
414
  while (i < names.length || j < nBase) {
271
415
  let name;
272
416
  let value;
273
- if (j === nBase || (i < names.length && names[i] <= baseNames[j])) {
274
- name = names[i];
275
- value = values[i++];
276
- if (j < nBase && name === baseNames[j]) j++;
417
+ if (
418
+ j === nBase ||
419
+ (i < names.length &&
420
+ /** @type {string} */ (names[i]) <= /** @type {string} */ (baseNames[j]))
421
+ ) {
422
+ name = /** @type {string} */ (names[i]);
423
+ value = /** @type {string} */ (values[i++]);
424
+ if (j < nBase && name === baseNames[j]) {
425
+ j++;
426
+ }
277
427
  } else {
278
- name = baseNames[j];
279
- value = baseValues[j++];
428
+ name = /** @type {string} */ (baseNames[j]);
429
+ value = /** @type {string} */ (baseValues[j++]);
280
430
  }
281
- if (name === 'x-amz-content-sha256') payloadHash = value; // a caller-supplied one replaces the computed one
431
+ if (name === 'x-amz-content-sha256') {
432
+ payloadHash = value;
433
+ } // a caller-supplied one replaces the computed one
282
434
  canonicalHeaders += `${name}:${value}\n`;
283
435
  signedHeaders += signedHeaders === '' ? name : ';' + name;
284
436
  }
285
437
  }
286
438
 
287
439
  const canonicalRequest = `${method}\n${canonicalPath(service, url.pathname)}\n${canonicalQuery(url.search)}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
288
- return { headers, canonicalRequest, datetime, date, region, service, credentialScope: scheme.scope(date, region, service), signedHeaders, algorithm: scheme.algorithm };
440
+ return {
441
+ headers,
442
+ canonicalRequest,
443
+ datetime,
444
+ date,
445
+ region,
446
+ service,
447
+ credentialScope: scheme.scope(date, region, service),
448
+ signedHeaders,
449
+ algorithm: scheme.algorithm,
450
+ };
289
451
  }
290
452
 
291
- export const stringToSign = (prepared, canonicalRequestHash) => `${prepared.algorithm}\n${prepared.datetime}\n${prepared.credentialScope}\n${canonicalRequestHash}`;
453
+ /** @typedef {{ algorithm: string, datetime: string, credentialScope: string }} StringToSignParts */
454
+
455
+ /**
456
+ * @param {StringToSignParts} prepared
457
+ * @param {string} canonicalRequestHash
458
+ */
459
+ export const stringToSign = (prepared, canonicalRequestHash) =>
460
+ `${prepared.algorithm}\n${prepared.datetime}\n${prepared.credentialScope}\n${canonicalRequestHash}`;
292
461
 
293
- /** Adds the authorization header to the headers to return. */
462
+ /**
463
+ * Adds the authorization header to the headers to return.
464
+ * @param {ReturnType<typeof prepare>} prepared
465
+ * @param {string} accessKeyId
466
+ * @param {string} signature
467
+ */
294
468
  export function authorize(prepared, accessKeyId, signature) {
295
469
  prepared.headers.authorization = `${prepared.algorithm} Credential=${accessKeyId}/${prepared.credentialScope}, SignedHeaders=${prepared.signedHeaders}, Signature=${signature}`;
296
470
  return prepared.headers;
297
471
  }
298
472
 
473
+ /**
474
+ * @param {(key: string | Uint8Array, data: string) => Uint8Array} hmac
475
+ * @returns {(secretAccessKey: string, date: string, region: string, service: string) => Uint8Array}
476
+ */
299
477
  export function createKeys(hmac) {
478
+ /** @type {Map<string, Uint8Array>} */
300
479
  const signingKeys = new Map();
301
480
  const maxSigningKeys = 1000;
302
481
 
303
482
  // the key used last is checked first, which skips building the lookup string on repeated calls
483
+ /** @type {string | undefined} */
304
484
  let lastSecret;
485
+ /** @type {string | undefined} */
305
486
  let lastDate;
487
+ /** @type {string | undefined} */
306
488
  let lastRegion;
489
+ /** @type {string | undefined} */
307
490
  let lastService;
491
+ /** @type {Uint8Array | undefined} */
308
492
  let lastKey;
309
493
 
310
494
  return function getSigningKey(secretAccessKey, date, region, service) {
311
- if (secretAccessKey === lastSecret && date === lastDate && region === lastRegion && service === lastService) return lastKey;
495
+ if (
496
+ secretAccessKey === lastSecret &&
497
+ date === lastDate &&
498
+ region === lastRegion &&
499
+ service === lastService
500
+ ) {
501
+ return /** @type {Uint8Array} */ (lastKey);
502
+ }
312
503
  const cacheKey = `${date}\n${region}\n${service}\n${secretAccessKey}`; // a region or service cannot contain a line break, so two scopes never share a key
313
504
  let signingKey = signingKeys.get(cacheKey);
314
505
  if (signingKey === undefined) {
315
- signingKey = hmac(hmac(hmac(hmac('AWS4' + secretAccessKey, date), region), service), 'aws4_request');
316
- if (signingKeys.size >= maxSigningKeys) signingKeys.delete(signingKeys.keys().next().value);
506
+ signingKey = hmac(
507
+ hmac(hmac(hmac('AWS4' + secretAccessKey, date), region), service),
508
+ 'aws4_request',
509
+ );
510
+ if (signingKeys.size >= maxSigningKeys) {
511
+ signingKeys.delete(/** @type {string} */ (signingKeys.keys().next().value));
512
+ }
317
513
  signingKeys.set(cacheKey, signingKey);
318
514
  }
319
515
  lastSecret = secretAccessKey;
@@ -335,7 +531,15 @@ export function createSigner({ hexHash, hmacHex, getSigningKey }) {
335
531
  return authorize(
336
532
  prepared,
337
533
  credentials.accessKeyId,
338
- hmacHex(getSigningKey(credentials.secretAccessKey, prepared.date, prepared.region, prepared.service), stringToSign(prepared, hexHash(prepared.canonicalRequest))),
534
+ hmacHex(
535
+ getSigningKey(
536
+ credentials.secretAccessKey,
537
+ prepared.date,
538
+ prepared.region,
539
+ prepared.service,
540
+ ),
541
+ stringToSign(prepared, hexHash(prepared.canonicalRequest)),
542
+ ),
339
543
  );
340
544
  };
341
545
  }
package/src/bun.js CHANGED
@@ -10,15 +10,26 @@ import { createEventSigner } from './events.js';
10
10
  const bun = /** @type {any} */ (globalThis).Bun;
11
11
 
12
12
  // the HMAC state for the signing key in use is built once and cloned for each signature
13
+ /** @type {Uint8Array | undefined} */
13
14
  let baseKey;
15
+ /** @type {any} */
14
16
  let base;
15
17
 
18
+ /**
19
+ * @param {string | Uint8Array} key
20
+ * @param {string} data
21
+ */
16
22
  const hmac = (key, data) => crypto.createHmac('sha256', key).update(data).digest();
17
23
  const getSigningKey = /* @__PURE__ */ createKeys(hmac);
24
+ /** @param {string | Uint8Array} data */
18
25
  const hexHash = (data) => bun.SHA256.hash(data, 'hex');
19
26
  const primitives = {
20
27
  hexHash,
21
28
  hmac,
29
+ /**
30
+ * @param {Uint8Array} key
31
+ * @param {string} data
32
+ */
22
33
  hmacHex: (key, data) => {
23
34
  if (key !== baseKey) {
24
35
  baseKey = key;