sigflint 1.0.1 → 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/index.d.ts CHANGED
@@ -26,13 +26,19 @@ export interface EventSigner {
26
26
  * Node.js, Bun and Deno return the headers directly. Cloudflare Workers and browsers have only promise-based
27
27
  * WebCrypto and return a promise. Always `await` the result and the same code runs everywhere.
28
28
  */
29
- export function getAuthHeaders(request: SignRequest, credentials: Credentials): MaybePromise<AuthHeaders>;
29
+ export function getAuthHeaders(
30
+ request: SignRequest,
31
+ credentials: Credentials,
32
+ ): MaybePromise<AuthHeaders>;
30
33
 
31
34
  /** A presigned URL: the original URL with the authentication parameters and the signature added. `request.expires` is the lifetime in seconds (default 3600). */
32
35
  export function presign(request: SignRequest, credentials: Credentials): MaybePromise<string>;
33
36
 
34
37
  /** SigV4a (multi-region) headers; `request.regionSet` is required, for example 'us-east-1,us-west-2' or '*'. */
35
- export function getAuthHeadersV4a(request: SignRequestV4a, credentials: Credentials): MaybePromise<AuthHeaders>;
38
+ export function getAuthHeadersV4a(
39
+ request: SignRequestV4a,
40
+ credentials: Credentials,
41
+ ): MaybePromise<AuthHeaders>;
36
42
 
37
43
  /** A SigV4a presigned URL; `request.regionSet` is required. */
38
44
  export function presignV4a(request: SignRequestV4a, credentials: Credentials): MaybePromise<string>;
package/package.json CHANGED
@@ -1,27 +1,41 @@
1
1
  {
2
2
  "name": "sigflint",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Fast, dependency-free AWS Signature Version 4 signing for every AWS service: headers, presigned URLs, SigV4a, chunked uploads and event streams. Node.js, Bun, Deno, Cloudflare Workers and browsers.",
5
5
  "keywords": [
6
+ "authorization",
6
7
  "aws",
7
- "s3",
8
- "sigv4",
9
- "sigv4a",
8
+ "bun",
9
+ "cloudflare-workers",
10
+ "deno",
10
11
  "presigned-url",
12
+ "s3",
11
13
  "signature",
12
14
  "signing",
13
- "authorization",
14
- "bun",
15
- "deno",
16
- "cloudflare-workers"
15
+ "sigv4",
16
+ "sigv4a"
17
17
  ],
18
+ "homepage": "https://github.com/bompus/sigflint#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/bompus/sigflint/issues"
21
+ },
18
22
  "license": "MIT",
19
23
  "author": "bompus",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/bompus/sigflint.git"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "dist",
31
+ "types",
32
+ "index.d.ts",
33
+ "README.md",
34
+ "LICENSE",
35
+ "CHANGELOG.md"
36
+ ],
20
37
  "type": "module",
21
38
  "sideEffects": false,
22
- "engines": {
23
- "node": ">=22.12.0"
24
- },
25
39
  "main": "./dist/node.js",
26
40
  "types": "./index.d.ts",
27
41
  "exports": {
@@ -40,44 +54,39 @@
40
54
  },
41
55
  "./package.json": "./package.json"
42
56
  },
43
- "files": [
44
- "src",
45
- "dist",
46
- "types",
47
- "index.d.ts",
48
- "README.md",
49
- "LICENSE",
50
- "CHANGELOG.md"
51
- ],
52
57
  "scripts": {
53
- "build": "esbuild src/node.js src/bun.js --bundle --format=esm --platform=node --target=es2022 --outdir=dist --log-level=warning",
58
+ "build": "bun build src/node.js --target=node --outfile=dist/node.js && bun build src/bun.js --target=bun --outfile=dist/bun.js",
54
59
  "pretest": "npm run build",
55
60
  "test": "node --test \"test/*.test.js\"",
56
61
  "pretest:bun": "npm run build",
57
62
  "test:bun": "bun test test/",
58
- "types": "tsc && tsc -p test/types",
63
+ "types": "tsc -p tsconfig.build.json && bun --check --tsconfig-override tsconfig.json && bun --check --tsconfig-override test/types/tsconfig.json",
64
+ "pretypes:streams": "npm ci --prefix conformance/streams",
65
+ "types:streams": "bun --check --tsconfig-override conformance/streams/tsconfig.json",
66
+ "pretypes:bench": "npm ci --prefix bench",
67
+ "types:bench": "bun --check --tsconfig-override bench/tsconfig.json",
59
68
  "conformance": "node conformance/build.mjs && node conformance/build-more.mjs",
60
- "prepack": "tsc && npm run build",
61
- "lint": "npx --yes oxlint@1.85.0 --deny-warnings src test conformance index.d.ts",
62
- "format": "npx --yes oxfmt@0.70.0 .",
63
- "check:style": "npm run lint && npx --yes oxfmt@0.70.0 --check .",
64
- "check": "npm test && npm run types && npm run check:package && npm run check:style",
69
+ "prepack": "tsc -p tsconfig.build.json && npm run build",
70
+ "lint": "tsc -p tsconfig.build.json && oxlint --deny-warnings",
71
+ "format": "bun fmt --no-script && ruff format . && git ls-files -z '*.sh' | xargs -0 shfmt -w",
72
+ "check:style": "bun fmt --no-script --check && npm run lint && npm run check:py && npm run check:sh",
73
+ "check": "npm run check:style && npm run types && npm run types:streams && npm run types:bench && npm test && npm run check:package",
65
74
  "check:benchmarked": "node bench/check-benchmarked.mjs",
66
- "check:package": "tsc && npm run build && publint && attw --pack . --profile esm-only"
75
+ "check:py": "ruff format --check . && ruff check .",
76
+ "check:sh": "git ls-files -z '*.sh' | xargs -0 shellcheck && git ls-files -z '*.sh' | xargs -0 shfmt -d -i 2 -bn -ci",
77
+ "check:package": "tsc -p tsconfig.build.json && npm run build && publint && attw --pack . --profile esm-only"
67
78
  },
68
79
  "devDependencies": {
69
80
  "@arethetypeswrong/cli": "^0.18.5",
81
+ "@types/bun": "1.4.3",
70
82
  "@types/node": "^26.6.5",
71
- "esbuild": "^0.28.2",
83
+ "oxfmt": "0.73.0",
84
+ "oxlint": "1.88.0",
85
+ "oxlint-tsgolint": "7.0.2004",
72
86
  "publint": "^0.3.25",
73
- "typescript": "^7.0.2"
74
- },
75
- "repository": {
76
- "type": "git",
77
- "url": "git+https://github.com/bompus/sigflint.git"
87
+ "typescript": "7.1.0-beta"
78
88
  },
79
- "bugs": {
80
- "url": "https://github.com/bompus/sigflint/issues"
81
- },
82
- "homepage": "https://github.com/bompus/sigflint#readme"
89
+ "engines": {
90
+ "node": ">=22.12.0"
91
+ }
83
92
  }
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;
@@ -219,13 +335,29 @@ 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
362
  const { url, signHeaders } = request;
231
363
  const method = request.method.toUpperCase(); // 'get' is signed as GET, which is what fetch sends and what botocore signs
@@ -237,31 +369,42 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
237
369
  const datetime = amzDatetime(request.now ?? Date.now());
238
370
  const date = datetime.slice(0, 8);
239
371
 
240
- 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
241
376
 
242
377
  const extraNames = signHeaders === undefined ? [] : Object.keys(signHeaders);
243
378
  let canonicalHeaders;
244
379
  let signedHeaders;
245
380
  if (extraNames.length === 0 && !sessionToken) {
246
381
  // the usual request: only the headers that are always signed, already in sorted order
247
- 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`;
248
385
  signedHeaders = withHash ? 'host;x-amz-content-sha256;x-amz-date' : 'host;x-amz-date';
249
386
  } else {
250
387
  // the extra headers (and the session token) in name order, then merged with the ones that are always signed;
251
388
  // a header named like one of those, or like an earlier extra, replaces it
389
+ /** @type {string[]} */
252
390
  const names = [];
391
+ /** @type {string[]} */
253
392
  const values = [];
254
393
  if (sessionToken) {
255
394
  headers['x-amz-security-token'] = sessionToken;
256
395
  insert(names, values, 'x-amz-security-token', sessionToken.trim());
257
396
  }
258
397
  for (const name of extraNames) {
259
- const value = /** @type {Record<string, string>} */ (signHeaders)[name];
398
+ const value = /** @type {string} */ (
399
+ /** @type {Record<string, string>} */ (signHeaders)[name]
400
+ );
260
401
  const lowerName = signHeaderName(name, value);
261
402
  headers[lowerName] = value;
262
403
  insert(names, values, lowerName, headerValue(value));
263
404
  }
264
- 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'];
265
408
  const baseValues = withHash ? [url.host, payloadHash, datetime] : [url.host, datetime];
266
409
  const nBase = baseNames.length;
267
410
  canonicalHeaders = '';
@@ -271,50 +414,102 @@ export function prepare(request, credentials, payloadHash, scheme = symmetric) {
271
414
  while (i < names.length || j < nBase) {
272
415
  let name;
273
416
  let value;
274
- if (j === nBase || (i < names.length && names[i] <= baseNames[j])) {
275
- name = names[i];
276
- value = values[i++];
277
- 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
+ }
278
427
  } else {
279
- name = baseNames[j];
280
- value = baseValues[j++];
428
+ name = /** @type {string} */ (baseNames[j]);
429
+ value = /** @type {string} */ (baseValues[j++]);
281
430
  }
282
- 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
283
434
  canonicalHeaders += `${name}:${value}\n`;
284
435
  signedHeaders += signedHeaders === '' ? name : ';' + name;
285
436
  }
286
437
  }
287
438
 
288
439
  const canonicalRequest = `${method}\n${canonicalPath(service, url.pathname)}\n${canonicalQuery(url.search)}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
289
- 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
+ };
290
451
  }
291
452
 
292
- 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}`;
293
461
 
294
- /** 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
+ */
295
468
  export function authorize(prepared, accessKeyId, signature) {
296
469
  prepared.headers.authorization = `${prepared.algorithm} Credential=${accessKeyId}/${prepared.credentialScope}, SignedHeaders=${prepared.signedHeaders}, Signature=${signature}`;
297
470
  return prepared.headers;
298
471
  }
299
472
 
473
+ /**
474
+ * @param {(key: string | Uint8Array, data: string) => Uint8Array} hmac
475
+ * @returns {(secretAccessKey: string, date: string, region: string, service: string) => Uint8Array}
476
+ */
300
477
  export function createKeys(hmac) {
478
+ /** @type {Map<string, Uint8Array>} */
301
479
  const signingKeys = new Map();
302
480
  const maxSigningKeys = 1000;
303
481
 
304
482
  // the key used last is checked first, which skips building the lookup string on repeated calls
483
+ /** @type {string | undefined} */
305
484
  let lastSecret;
485
+ /** @type {string | undefined} */
306
486
  let lastDate;
487
+ /** @type {string | undefined} */
307
488
  let lastRegion;
489
+ /** @type {string | undefined} */
308
490
  let lastService;
491
+ /** @type {Uint8Array | undefined} */
309
492
  let lastKey;
310
493
 
311
494
  return function getSigningKey(secretAccessKey, date, region, service) {
312
- 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
+ }
313
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
314
504
  let signingKey = signingKeys.get(cacheKey);
315
505
  if (signingKey === undefined) {
316
- signingKey = hmac(hmac(hmac(hmac('AWS4' + secretAccessKey, date), region), service), 'aws4_request');
317
- 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
+ }
318
513
  signingKeys.set(cacheKey, signingKey);
319
514
  }
320
515
  lastSecret = secretAccessKey;
@@ -336,7 +531,15 @@ export function createSigner({ hexHash, hmacHex, getSigningKey }) {
336
531
  return authorize(
337
532
  prepared,
338
533
  credentials.accessKeyId,
339
- 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
+ ),
340
543
  );
341
544
  };
342
545
  }