@sdxc/digest-fields 2026.10.23 → 2026.10.25
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 +11 -4
- package/dist/decode.d.ts +23 -0
- package/dist/decode.js +79 -0
- package/dist/digest.d.ts +2 -1
- package/dist/digest.js +2 -1
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/parse.d.ts +8 -2
- package/dist/parse.js +9 -1
- package/dist/stringify.js +2 -3
- package/dist/types.d.ts +7 -5
- package/dist/verify.d.ts +10 -2
- package/dist/verify.js +20 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
RFC 9530 `Content-Digest` and `Repr-Digest`, plus the RFC 3230 `Digest` header.
|
|
4
4
|
|
|
5
|
-
Compute, write, read and verify the HTTP digest fields, including the `Want-Content-Digest` and `Want-Repr-Digest` preferences and the legacy `Digest` header that draft-cavage HTTP signatures cover.
|
|
5
|
+
Compute, write, read and verify the HTTP digest fields, including the `Want-Content-Digest` and `Want-Repr-Digest` preferences, `Unencoded-Digest` and `Want-Unencoded-Digest` from [HTTP Unencoded Digest](https://datatracker.ietf.org/doc/draft-ietf-httpbis-unencoded-digest/), and the legacy `Digest` header that draft-cavage HTTP signatures cover.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -59,6 +59,12 @@ let checked = await verify(request.headers, await request.bytes(), { field: "con
|
|
|
59
59
|
if (isFailure(checked)) return new Response(checked.error.message, { status: 400 });
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
`Unencoded-Digest` covers the representation before content coding. Pass `decode: true` when the body is still coded as `Content-Encoding` says (`gzip`, `x-gzip`, `deflate`, `identity`), and leave it off for a body `fetch` already decoded:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
await verify(request.headers, await request.bytes(), { field: "unencoded-digest", decode: true });
|
|
66
|
+
```
|
|
67
|
+
|
|
62
68
|
## API
|
|
63
69
|
|
|
64
70
|
### `digest(bytes: BinaryLike, algorithm: DigestAlgorithm): Promise<Result<Uint8Array, DigestError>>`
|
|
@@ -81,7 +87,7 @@ Writes a field. `Digest` uppercases algorithm names (`SHA-256=…`), the form dr
|
|
|
81
87
|
|
|
82
88
|
### `verify(headers: Headers, body: BinaryLike, options: VerifyOptions): Promise<Result<void, DigestError>>`
|
|
83
89
|
|
|
84
|
-
Checks a body against `options.field`: `content-digest`, `repr-digest` or `digest`. Every `sha-256` and `sha-512` entry must match, so a forged entry next to a valid one still fails; entries for other algorithms are skipped.
|
|
90
|
+
Checks a body against `options.field`: `content-digest`, `repr-digest`, `unencoded-digest` or `digest`. `options.decode` removes the `Content-Encoding` codings before checking `unencoded-digest`; `decodeContent(body, contentEncoding)` does the same on its own. Every `sha-256` and `sha-512` entry must match, so a forged entry next to a valid one still fails; entries for other algorithms are skipped.
|
|
85
91
|
|
|
86
92
|
### `DIGEST_ALGORITHMS`
|
|
87
93
|
|
|
@@ -98,6 +104,7 @@ The failure every function answers with; `code` says which check failed:
|
|
|
98
104
|
| `invalid` | `stringify` got a name the field cannot carry or a bad weight |
|
|
99
105
|
| `unsupported-algorithm` | No `sha-256`/`sha-512` entry to check, or `digest` got another |
|
|
100
106
|
| `mismatch` | The body differs from a digest |
|
|
107
|
+
| `coding` | `decode` met an unknown content coding or a corrupt body |
|
|
101
108
|
| `crypto` | The runtime refused the hash |
|
|
102
109
|
|
|
103
110
|
### Types
|
|
@@ -108,7 +115,7 @@ The failure every function answers with; `code` says which check failed:
|
|
|
108
115
|
|
|
109
116
|
#### `DigestField` and `DigestValueField`
|
|
110
117
|
|
|
111
|
-
`"content-digest" | "repr-digest" | "want-content-digest" | "want-repr-digest" | "digest"`. `DigestValueField` is the
|
|
118
|
+
`"content-digest" | "repr-digest" | "unencoded-digest" | "want-content-digest" | "want-repr-digest" | "want-unencoded-digest" | "digest"`. `DigestValueField` is the four that carry digests.
|
|
112
119
|
|
|
113
120
|
#### `Digests`, `Preferences` and `FieldValues`
|
|
114
121
|
|
|
@@ -120,7 +127,7 @@ The union of `DigestError` codes in the table above.
|
|
|
120
127
|
|
|
121
128
|
#### `VerifyOptions`
|
|
122
129
|
|
|
123
|
-
`{ field: DigestValueField }`.
|
|
130
|
+
`{ field: DigestValueField; decode?: boolean }`.
|
|
124
131
|
|
|
125
132
|
## Pattern: Signing-Ready Outbound Request
|
|
126
133
|
|
package/dist/decode.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the content codings a `Content-Encoding` field lists, recovering the unencoded
|
|
3
|
+
* representation an `Unencoded-Digest` covers, so a receiver holding still-coded bytes can
|
|
4
|
+
* verify that field without re-encoding anything.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { BinaryLike, Bytes } from "@sdxc/crypto";
|
|
10
|
+
import type { Result } from "@sdxc/result";
|
|
11
|
+
import { DigestError } from "./errors.js";
|
|
12
|
+
/**
|
|
13
|
+
* Decodes a body through every coding a `Content-Encoding` value lists, last applied first,
|
|
14
|
+
* as the codings were stacked in the listed order. An empty value or `identity` leaves the
|
|
15
|
+
* bytes as they are.
|
|
16
|
+
*
|
|
17
|
+
* @param body - The content as received, still coded.
|
|
18
|
+
* @param contentEncoding - The `Content-Encoding` value, or `null` when the field is absent.
|
|
19
|
+
* @returns The unencoded bytes, or `coding` when a coding is not `gzip`, `x-gzip`, `deflate`
|
|
20
|
+
* or `identity`, or when the body does not decode.
|
|
21
|
+
* @example await decodeContent(gzipped, "gzip") // success(the original bytes)
|
|
22
|
+
*/
|
|
23
|
+
export declare function decodeContent(body: BinaryLike, contentEncoding: string | null): Promise<Result<Bytes, DigestError>>;
|
package/dist/decode.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the content codings a `Content-Encoding` field lists, recovering the unencoded
|
|
3
|
+
* representation an `Unencoded-Digest` covers, so a receiver holding still-coded bytes can
|
|
4
|
+
* verify that field without re-encoding anything.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { failure, isFailure, success } from "@sdxc/result";
|
|
10
|
+
import { DigestError } from "./errors.js";
|
|
11
|
+
/** Shared UTF-8 encoder, so a text body turns into the same bytes `digest` hashes. */
|
|
12
|
+
const ENCODER = new TextEncoder();
|
|
13
|
+
/** Each decodable content coding, keyed by its registered name, with its stream format. */
|
|
14
|
+
const DECODERS = {
|
|
15
|
+
identity: null,
|
|
16
|
+
gzip: "gzip",
|
|
17
|
+
"x-gzip": "gzip",
|
|
18
|
+
deflate: "deflate",
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Decodes a body through every coding a `Content-Encoding` value lists, last applied first,
|
|
22
|
+
* as the codings were stacked in the listed order. An empty value or `identity` leaves the
|
|
23
|
+
* bytes as they are.
|
|
24
|
+
*
|
|
25
|
+
* @param body - The content as received, still coded.
|
|
26
|
+
* @param contentEncoding - The `Content-Encoding` value, or `null` when the field is absent.
|
|
27
|
+
* @returns The unencoded bytes, or `coding` when a coding is not `gzip`, `x-gzip`, `deflate`
|
|
28
|
+
* or `identity`, or when the body does not decode.
|
|
29
|
+
* @example await decodeContent(gzipped, "gzip") // success(the original bytes)
|
|
30
|
+
*/
|
|
31
|
+
export async function decodeContent(body, contentEncoding) {
|
|
32
|
+
let codings = (contentEncoding ?? "")
|
|
33
|
+
.split(",")
|
|
34
|
+
.map((coding) => coding.trim().toLowerCase())
|
|
35
|
+
.filter((coding) => coding !== "")
|
|
36
|
+
.reverse();
|
|
37
|
+
let bytes = toBytes(body);
|
|
38
|
+
for (let coding of codings) {
|
|
39
|
+
if (!Object.hasOwn(DECODERS, coding)) {
|
|
40
|
+
return failure(new DigestError("coding", `Cannot decode the ${coding} content coding`));
|
|
41
|
+
}
|
|
42
|
+
let format = DECODERS[coding];
|
|
43
|
+
if (!format)
|
|
44
|
+
continue;
|
|
45
|
+
let decoded = await decompress(bytes, format);
|
|
46
|
+
if (isFailure(decoded))
|
|
47
|
+
return decoded;
|
|
48
|
+
bytes = decoded.data;
|
|
49
|
+
}
|
|
50
|
+
return success(bytes);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Runs bytes through one platform decompression stream, turning a corrupt body into a
|
|
54
|
+
* `coding` failure.
|
|
55
|
+
*
|
|
56
|
+
* @param bytes - The coded bytes.
|
|
57
|
+
* @param format - The stream format of the coding.
|
|
58
|
+
*/
|
|
59
|
+
async function decompress(bytes, format) {
|
|
60
|
+
try {
|
|
61
|
+
let stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream(format));
|
|
62
|
+
return success(new Uint8Array(await new Response(stream).arrayBuffer()));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return failure(new DigestError("coding", `The body does not decode as ${format}`, { cause: error }));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Normalizes a body into bytes, reading text as UTF-8.
|
|
70
|
+
*
|
|
71
|
+
* @param body - Text or binary content.
|
|
72
|
+
*/
|
|
73
|
+
function toBytes(body) {
|
|
74
|
+
if (typeof body === "string")
|
|
75
|
+
return ENCODER.encode(body);
|
|
76
|
+
if (body instanceof ArrayBuffer)
|
|
77
|
+
return new Uint8Array(body);
|
|
78
|
+
return body.slice();
|
|
79
|
+
}
|
package/dist/digest.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ export declare function isDigestAlgorithm(name: string): name is DigestAlgorithm
|
|
|
21
21
|
* Digests a body. Text is read as UTF-8, so pass the exact bytes sent on the wire when a
|
|
22
22
|
* body is not UTF-8 text.
|
|
23
23
|
*
|
|
24
|
-
* @param bytes - The content (for `Content-Digest`)
|
|
24
|
+
* @param bytes - The content (for `Content-Digest`), the representation (for `Repr-Digest`)
|
|
25
|
+
* or the representation before content coding (for `Unencoded-Digest`).
|
|
25
26
|
* @param algorithm - `sha-256` or `sha-512`.
|
|
26
27
|
* @returns The digest bytes, `unsupported-algorithm` for any other name, or `crypto` when
|
|
27
28
|
* the runtime refuses the hash.
|
package/dist/digest.js
CHANGED
|
@@ -22,7 +22,8 @@ export function isDigestAlgorithm(name) {
|
|
|
22
22
|
* Digests a body. Text is read as UTF-8, so pass the exact bytes sent on the wire when a
|
|
23
23
|
* body is not UTF-8 text.
|
|
24
24
|
*
|
|
25
|
-
* @param bytes - The content (for `Content-Digest`)
|
|
25
|
+
* @param bytes - The content (for `Content-Digest`), the representation (for `Repr-Digest`)
|
|
26
|
+
* or the representation before content coding (for `Unencoded-Digest`).
|
|
26
27
|
* @param algorithm - `sha-256` or `sha-512`.
|
|
27
28
|
* @returns The digest bytes, `unsupported-algorithm` for any other name, or `crypto` when
|
|
28
29
|
* the runtime refuses the hash.
|
package/dist/errors.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Why a digest field could not be read, written or verified. `mismatch` is the only code
|
|
11
11
|
* that says the content changed; every other code is about the field itself.
|
|
12
12
|
*/
|
|
13
|
-
export type DigestErrorCode = "missing" | "malformed" | "invalid" | "unsupported-algorithm" | "mismatch" | "crypto";
|
|
13
|
+
export type DigestErrorCode = "missing" | "malformed" | "invalid" | "unsupported-algorithm" | "mismatch" | "coding" | "crypto";
|
|
14
14
|
/** Returned inside a `Failure` by every function in this package, never thrown. */
|
|
15
15
|
export declare class DigestError extends Error {
|
|
16
16
|
name: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reads, writes and verifies the HTTP digest fields: RFC 9530's `Content-Digest
|
|
3
|
-
* `Repr-Digest`
|
|
4
|
-
* draft-cavage HTTP signatures still cover.
|
|
2
|
+
* Reads, writes and verifies the HTTP digest fields: RFC 9530's `Content-Digest` and
|
|
3
|
+
* `Repr-Digest`, HTTP Unencoded Digest's `Unencoded-Digest`, their `Want-*` preferences,
|
|
4
|
+
* and RFC 3230's legacy `Digest` that draft-cavage HTTP signatures still cover.
|
|
5
5
|
*
|
|
6
6
|
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
@@ -10,6 +10,7 @@ export type { DigestErrorCode } from "./errors.js";
|
|
|
10
10
|
export type { DigestAlgorithm, DigestField, DigestValueField, Digests, FieldValues, Preferences, } from "./types.js";
|
|
11
11
|
export type { VerifyOptions } from "./verify.js";
|
|
12
12
|
export { DIGEST_ALGORITHMS, digest } from "./digest.js";
|
|
13
|
+
export { decodeContent } from "./decode.js";
|
|
13
14
|
export { DigestError } from "./errors.js";
|
|
14
15
|
export { parse } from "./parse.js";
|
|
15
16
|
export { stringify } from "./stringify.js";
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reads, writes and verifies the HTTP digest fields: RFC 9530's `Content-Digest
|
|
3
|
-
* `Repr-Digest`
|
|
4
|
-
* draft-cavage HTTP signatures still cover.
|
|
2
|
+
* Reads, writes and verifies the HTTP digest fields: RFC 9530's `Content-Digest` and
|
|
3
|
+
* `Repr-Digest`, HTTP Unencoded Digest's `Unencoded-Digest`, their `Want-*` preferences,
|
|
4
|
+
* and RFC 3230's legacy `Digest` that draft-cavage HTTP signatures still cover.
|
|
5
5
|
*
|
|
6
6
|
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
9
|
export { DIGEST_ALGORITHMS, digest } from "./digest.js";
|
|
10
|
+
export { decodeContent } from "./decode.js";
|
|
10
11
|
export { DigestError } from "./errors.js";
|
|
11
12
|
export { parse } from "./parse.js";
|
|
12
13
|
export { stringify } from "./stringify.js";
|
package/dist/parse.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ import { DigestError } from "./errors.js";
|
|
|
12
12
|
/**
|
|
13
13
|
* Parses a field value into its model.
|
|
14
14
|
*
|
|
15
|
-
* `Content-Digest` and `
|
|
16
|
-
* `Want-*` fields answer preference weights. `Digest` reads algorithm names in any case
|
|
15
|
+
* `Content-Digest`, `Repr-Digest` and `Unencoded-Digest` answer digest bytes keyed by
|
|
16
|
+
* algorithm, and the `Want-*` fields answer preference weights. `Digest` reads algorithm names in any case
|
|
17
17
|
* and keys them lowercase; an entry for an algorithm outside `sha-256`/`sha-512` whose
|
|
18
18
|
* value is not base64 (RFC 3230's `UNIXsum`, say) is left out.
|
|
19
19
|
*
|
|
@@ -24,6 +24,12 @@ import { DigestError } from "./errors.js";
|
|
|
24
24
|
* @example parse("SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", "digest")
|
|
25
25
|
*/
|
|
26
26
|
export declare function parse<Field extends DigestField>(value: string, field: Field): Result<FieldValues[Field], DigestError>;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a field is one of the Structured Field Dictionaries whose members are digests.
|
|
29
|
+
*
|
|
30
|
+
* @param field - The field name, lowercase.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isDigestsField(field: DigestField): boolean;
|
|
27
33
|
/**
|
|
28
34
|
* Whether a bare item is a valid preference weight.
|
|
29
35
|
*
|
package/dist/parse.js
CHANGED
|
@@ -16,7 +16,7 @@ const MAX_PREFERENCE = 10;
|
|
|
16
16
|
export function parse(value, field) {
|
|
17
17
|
if (field === "digest")
|
|
18
18
|
return parseLegacy(value);
|
|
19
|
-
if (field
|
|
19
|
+
if (isDigestsField(field))
|
|
20
20
|
return parseDigests(value, field);
|
|
21
21
|
return parsePreferences(value, field);
|
|
22
22
|
}
|
|
@@ -104,6 +104,14 @@ function parseLegacy(value) {
|
|
|
104
104
|
}
|
|
105
105
|
return success(Object.fromEntries(entries));
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether a field is one of the Structured Field Dictionaries whose members are digests.
|
|
109
|
+
*
|
|
110
|
+
* @param field - The field name, lowercase.
|
|
111
|
+
*/
|
|
112
|
+
export function isDigestsField(field) {
|
|
113
|
+
return field === "content-digest" || field === "repr-digest" || field === "unencoded-digest";
|
|
114
|
+
}
|
|
107
115
|
/**
|
|
108
116
|
* Whether a bare item is a valid preference weight.
|
|
109
117
|
*
|
package/dist/stringify.js
CHANGED
|
@@ -10,15 +10,14 @@ import { Base64 } from "@sdxc/crypto";
|
|
|
10
10
|
import { failure, isFailure, success } from "@sdxc/result";
|
|
11
11
|
import { stringify as stringifyStructured } from "@sdxc/structured-fields";
|
|
12
12
|
import { DigestError } from "./errors.js";
|
|
13
|
-
import { isPreference } from "./parse.js";
|
|
13
|
+
import { isDigestsField, isPreference } from "./parse.js";
|
|
14
14
|
/** An RFC 3230 algorithm name: a token, which keeps `=` and `,` out of it. */
|
|
15
15
|
const LEGACY_ALGORITHM_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
16
16
|
export function stringify(value, field) {
|
|
17
17
|
if (field === "digest")
|
|
18
18
|
return stringifyLegacy(value);
|
|
19
|
-
if (field
|
|
19
|
+
if (isDigestsField(field))
|
|
20
20
|
return stringifyDictionary(value, field);
|
|
21
|
-
}
|
|
22
21
|
for (let [algorithm, weight] of Object.entries(value)) {
|
|
23
22
|
if (!isPreference(weight)) {
|
|
24
23
|
return failure(new DigestError("invalid", `${algorithm} must weigh an integer from 0 to 10`));
|
package/dist/types.d.ts
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
/** The algorithms `digest` and `verify` compute, spelled as RFC 9530 registers them. */
|
|
10
10
|
export type DigestAlgorithm = "sha-256" | "sha-512";
|
|
11
11
|
/**
|
|
12
|
-
* The fields this package reads and writes: the four of RFC 9530,
|
|
13
|
-
* `Digest`, which draft-cavage HTTP signatures cover.
|
|
12
|
+
* The fields this package reads and writes: the four of RFC 9530, the HTTP Unencoded Digest
|
|
13
|
+
* pair, and RFC 3230's legacy `Digest`, which draft-cavage HTTP signatures cover.
|
|
14
14
|
*/
|
|
15
|
-
export type DigestField = "content-digest" | "repr-digest" | "want-content-digest" | "want-repr-digest" | "digest";
|
|
15
|
+
export type DigestField = "content-digest" | "repr-digest" | "unencoded-digest" | "want-content-digest" | "want-repr-digest" | "want-unencoded-digest" | "digest";
|
|
16
16
|
/** The fields that carry digests of a body, as opposed to preferences for one. */
|
|
17
|
-
export type DigestValueField = "content-digest" | "repr-digest" | "digest";
|
|
17
|
+
export type DigestValueField = "content-digest" | "repr-digest" | "unencoded-digest" | "digest";
|
|
18
18
|
/**
|
|
19
19
|
* Digest bytes keyed by lowercase algorithm name. Algorithms this package does not compute
|
|
20
20
|
* are kept, so a field round-trips; `verify` checks only `sha-256` and `sha-512`.
|
|
@@ -22,14 +22,16 @@ export type DigestValueField = "content-digest" | "repr-digest" | "digest";
|
|
|
22
22
|
export type Digests = Record<string, Uint8Array>;
|
|
23
23
|
/**
|
|
24
24
|
* Preference weights keyed by lowercase algorithm name, from `0` (not acceptable) to `10`
|
|
25
|
-
* (most preferred), as `Want
|
|
25
|
+
* (most preferred), as the `Want-*-Digest` fields carry them.
|
|
26
26
|
*/
|
|
27
27
|
export type Preferences = Record<string, number>;
|
|
28
28
|
/** What each field parses to and what `stringify` accepts for it. */
|
|
29
29
|
export interface FieldValues {
|
|
30
30
|
"content-digest": Digests;
|
|
31
31
|
"repr-digest": Digests;
|
|
32
|
+
"unencoded-digest": Digests;
|
|
32
33
|
digest: Digests;
|
|
33
34
|
"want-content-digest": Preferences;
|
|
34
35
|
"want-repr-digest": Preferences;
|
|
36
|
+
"want-unencoded-digest": Preferences;
|
|
35
37
|
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -12,8 +12,15 @@ import type { DigestValueField } from "./types.js";
|
|
|
12
12
|
import { DigestError } from "./errors.js";
|
|
13
13
|
/** What `verify` needs besides the headers and the body. */
|
|
14
14
|
export interface VerifyOptions {
|
|
15
|
-
/** The field to check, lowercase: `content-digest`, `repr-digest` or
|
|
15
|
+
/** The field to check, lowercase: `content-digest`, `repr-digest`, `unencoded-digest` or `digest`. */
|
|
16
16
|
field: DigestValueField;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the body is still coded as `Content-Encoding` says, for `unencoded-digest`:
|
|
19
|
+
* `verify` then decodes it before hashing. Leave it off for a body already decoded,
|
|
20
|
+
* as `fetch` hands a response's.
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
decode?: boolean;
|
|
17
24
|
}
|
|
18
25
|
/**
|
|
19
26
|
* Verifies a body against a digest field. Every `sha-256` and `sha-512` entry the field
|
|
@@ -24,7 +31,8 @@ export interface VerifyOptions {
|
|
|
24
31
|
* @param body - The exact bytes received; text is read as UTF-8.
|
|
25
32
|
* @param options - Which field to check.
|
|
26
33
|
* @returns Success when every supported entry matches, else `missing`, `malformed`,
|
|
27
|
-
* `unsupported-algorithm` (no `sha-256` or `sha-512` entry), `mismatch`
|
|
34
|
+
* `unsupported-algorithm` (no `sha-256` or `sha-512` entry), `mismatch`, `coding` (the
|
|
35
|
+
* body does not decode) or `crypto`.
|
|
28
36
|
* @example await verify(request.headers, body, { field: "content-digest" })
|
|
29
37
|
*/
|
|
30
38
|
export declare function verify(headers: Headers, body: BinaryLike, options: VerifyOptions): Promise<Result<void, DigestError>>;
|
package/dist/verify.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { timingSafeEqual } from "@sdxc/crypto";
|
|
10
10
|
import { failure, isFailure, success } from "@sdxc/result";
|
|
11
|
+
import { decodeContent } from "./decode.js";
|
|
11
12
|
import { digest, isDigestAlgorithm } from "./digest.js";
|
|
12
13
|
import { DigestError } from "./errors.js";
|
|
13
14
|
import { parse } from "./parse.js";
|
|
@@ -20,7 +21,8 @@ import { parse } from "./parse.js";
|
|
|
20
21
|
* @param body - The exact bytes received; text is read as UTF-8.
|
|
21
22
|
* @param options - Which field to check.
|
|
22
23
|
* @returns Success when every supported entry matches, else `missing`, `malformed`,
|
|
23
|
-
* `unsupported-algorithm` (no `sha-256` or `sha-512` entry), `mismatch`
|
|
24
|
+
* `unsupported-algorithm` (no `sha-256` or `sha-512` entry), `mismatch`, `coding` (the
|
|
25
|
+
* body does not decode) or `crypto`.
|
|
24
26
|
* @example await verify(request.headers, body, { field: "content-digest" })
|
|
25
27
|
*/
|
|
26
28
|
export async function verify(headers, body, options) {
|
|
@@ -30,11 +32,14 @@ export async function verify(headers, body, options) {
|
|
|
30
32
|
let parsed = parse(text, options.field);
|
|
31
33
|
if (isFailure(parsed))
|
|
32
34
|
return parsed;
|
|
35
|
+
let content = await hashedContent(headers, body, options);
|
|
36
|
+
if (isFailure(content))
|
|
37
|
+
return content;
|
|
33
38
|
let checked = 0;
|
|
34
39
|
for (let [algorithm, expected] of Object.entries(parsed.data)) {
|
|
35
40
|
if (!isDigestAlgorithm(algorithm))
|
|
36
41
|
continue;
|
|
37
|
-
let actual = await digest(
|
|
42
|
+
let actual = await digest(content.data, algorithm);
|
|
38
43
|
if (isFailure(actual))
|
|
39
44
|
return actual;
|
|
40
45
|
if (!timingSafeEqual(actual.data, expected)) {
|
|
@@ -47,3 +52,16 @@ export async function verify(headers, body, options) {
|
|
|
47
52
|
}
|
|
48
53
|
return success(undefined);
|
|
49
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The bytes a field's digests cover: the body as received, or for `unencoded-digest` with
|
|
57
|
+
* `decode` set, the body with every listed content coding removed.
|
|
58
|
+
*
|
|
59
|
+
* @param headers - The message headers holding `Content-Encoding`.
|
|
60
|
+
* @param body - The body as received.
|
|
61
|
+
* @param options - Which field to check and whether to decode.
|
|
62
|
+
*/
|
|
63
|
+
async function hashedContent(headers, body, options) {
|
|
64
|
+
if (options.field !== "unencoded-digest" || !options.decode)
|
|
65
|
+
return success(body);
|
|
66
|
+
return decodeContent(body, headers.get("content-encoding"));
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/digest-fields",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.25",
|
|
4
4
|
"description": "RFC 9530 Content-Digest and Repr-Digest, plus the RFC 3230 Digest header",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@sdxc/crypto": "2026.10.23",
|
|
12
12
|
"@sdxc/result": "2026.10.13",
|
|
13
|
-
"@sdxc/structured-fields": "2026.10.
|
|
13
|
+
"@sdxc/structured-fields": "2026.10.25"
|
|
14
14
|
},
|
|
15
|
-
"gitHead": "
|
|
15
|
+
"gitHead": "1440b969c0a3e3808dfcee3f17d2ef43915f32a8",
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|