@sdxc/digest-fields 2026.10.9 → 2026.10.13
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 +102 -26
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @sdxc/digest-fields
|
|
2
2
|
|
|
3
|
-
RFC 9530 `Content-Digest
|
|
3
|
+
RFC 9530 `Content-Digest` and `Repr-Digest`, plus the RFC 3230 `Digest` header.
|
|
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.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -12,23 +14,27 @@ Every function answers a `Result` from [`@sdxc/result`](https://www.npmjs.com/pa
|
|
|
12
14
|
|
|
13
15
|
## Usage
|
|
14
16
|
|
|
17
|
+
### Digest a body
|
|
18
|
+
|
|
15
19
|
```typescript
|
|
16
|
-
import { digest
|
|
17
|
-
import {
|
|
20
|
+
import { digest } from "@sdxc/digest-fields";
|
|
21
|
+
import { unwrap } from "@sdxc/result";
|
|
18
22
|
|
|
19
|
-
let body = new TextEncoder().encode(JSON.stringify(
|
|
20
|
-
let bytes = unwrap(await digest(body, "sha-256"));
|
|
23
|
+
let body = new TextEncoder().encode(JSON.stringify({ message: "Hello" }));
|
|
24
|
+
let bytes = unwrap(await digest(body, "sha-256")); // Uint8Array(32)
|
|
25
|
+
```
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
"content-digest": unwrap(stringify({ "sha-256": bytes }, "content-digest")), // "sha-256=:…:"
|
|
24
|
-
digest: unwrap(stringify({ "sha-256": bytes }, "digest")), // "SHA-256=…"
|
|
25
|
-
});
|
|
27
|
+
### Write the field
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
```typescript
|
|
30
|
+
import { stringify } from "@sdxc/digest-fields";
|
|
31
|
+
|
|
32
|
+
stringify({ "sha-256": bytes }, "content-digest"); // success("sha-256=:…:")
|
|
33
|
+
stringify({ "sha-256": bytes }, "digest"); // success("SHA-256=…")
|
|
34
|
+
stringify({ "sha-512": 3, "sha-256": 10 }, "want-content-digest"); // success("sha-512=3, sha-256=10")
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
### Read a field
|
|
32
38
|
|
|
33
39
|
```typescript
|
|
34
40
|
import { parse } from "@sdxc/digest-fields";
|
|
@@ -36,15 +42,28 @@ import { parse } from "@sdxc/digest-fields";
|
|
|
36
42
|
parse("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", "content-digest");
|
|
37
43
|
// success({ "sha-256": Uint8Array(32) })
|
|
38
44
|
|
|
45
|
+
parse("SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", "digest");
|
|
46
|
+
// success({ "sha-256": Uint8Array(32) })
|
|
47
|
+
|
|
39
48
|
parse("sha-512=3, sha-256=10", "want-content-digest");
|
|
40
49
|
// success({ "sha-512": 3, "sha-256": 10 })
|
|
41
50
|
```
|
|
42
51
|
|
|
52
|
+
### Verify a received body
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { verify } from "@sdxc/digest-fields";
|
|
56
|
+
import { isFailure } from "@sdxc/result";
|
|
57
|
+
|
|
58
|
+
let checked = await verify(request.headers, await request.bytes(), { field: "content-digest" });
|
|
59
|
+
if (isFailure(checked)) return new Response(checked.error.message, { status: 400 });
|
|
60
|
+
```
|
|
61
|
+
|
|
43
62
|
## API
|
|
44
63
|
|
|
45
64
|
### `digest(bytes: BinaryLike, algorithm: DigestAlgorithm): Promise<Result<Uint8Array, DigestError>>`
|
|
46
65
|
|
|
47
|
-
Hashes a body with `sha-256` or `sha-512`.
|
|
66
|
+
Hashes a body with `sha-256` or `sha-512`. `BinaryLike` is `string | Uint8Array | ArrayBuffer`; text is read as UTF-8, so pass the exact bytes sent when the body is not UTF-8 text.
|
|
48
67
|
|
|
49
68
|
```typescript
|
|
50
69
|
let bytes = await digest(body, "sha-256");
|
|
@@ -54,15 +73,15 @@ let bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", body));
|
|
|
54
73
|
|
|
55
74
|
### `parse(value: string, field: DigestField): Result<FieldValues[field], DigestError>`
|
|
56
75
|
|
|
57
|
-
Reads a field into digest bytes (`content-digest`, `repr-digest`, `digest`) or preference weights from 0 to 10 (`want-content-digest`, `want-repr-digest`), keyed by lowercase algorithm name. The RFC 9530 fields
|
|
76
|
+
Reads a field into digest bytes (`content-digest`, `repr-digest`, `digest`) or preference weights from 0 to 10 (`want-content-digest`, `want-repr-digest`), keyed by lowercase algorithm name. The RFC 9530 fields follow the Structured Field grammar of [`@sdxc/structured-fields`](https://www.npmjs.com/package/@sdxc/structured-fields). `Digest` accepts algorithm names in any case, fails on a repeated algorithm, and skips an entry for another algorithm whose value is not base64, such as `UNIXsum=30637`.
|
|
58
77
|
|
|
59
78
|
### `stringify(value: FieldValues[field], field: DigestField): Result<string, DigestError>`
|
|
60
79
|
|
|
61
|
-
Writes a field. `Digest` uppercases algorithm names (`SHA-256=…`),
|
|
80
|
+
Writes a field. `Digest` uppercases algorithm names (`SHA-256=…`), the form draft-cavage verifiers read. An empty value answers `""`, meaning "do not send the field".
|
|
62
81
|
|
|
63
|
-
### `verify(headers: Headers, body: BinaryLike, options:
|
|
82
|
+
### `verify(headers: Headers, body: BinaryLike, options: VerifyOptions): Promise<Result<void, DigestError>>`
|
|
64
83
|
|
|
65
|
-
Checks a body against `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
|
|
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.
|
|
66
85
|
|
|
67
86
|
### `DIGEST_ALGORITHMS`
|
|
68
87
|
|
|
@@ -70,7 +89,7 @@ Checks a body against `content-digest`, `repr-digest` or `digest`. Every `sha-25
|
|
|
70
89
|
|
|
71
90
|
### `DigestError`
|
|
72
91
|
|
|
73
|
-
The failure every function answers with
|
|
92
|
+
The failure every function answers with; `code` says which check failed:
|
|
74
93
|
|
|
75
94
|
| `code` | When |
|
|
76
95
|
| ----------------------- | -------------------------------------------------------------- |
|
|
@@ -87,14 +106,69 @@ The failure every function answers with. `code` says which check failed:
|
|
|
87
106
|
|
|
88
107
|
`"sha-256" | "sha-512"`.
|
|
89
108
|
|
|
90
|
-
#### `DigestField`
|
|
109
|
+
#### `DigestField` and `DigestValueField`
|
|
91
110
|
|
|
92
111
|
`"content-digest" | "repr-digest" | "want-content-digest" | "want-repr-digest" | "digest"`. `DigestValueField` is the three that carry digests.
|
|
93
112
|
|
|
94
|
-
#### `Digests` and `
|
|
113
|
+
#### `Digests`, `Preferences` and `FieldValues`
|
|
95
114
|
|
|
96
115
|
`Record<string, Uint8Array>` and `Record<string, number>`, keyed by lowercase algorithm name. `FieldValues` maps each field to the one it carries.
|
|
97
116
|
|
|
117
|
+
#### `DigestErrorCode`
|
|
118
|
+
|
|
119
|
+
The union of `DigestError` codes in the table above.
|
|
120
|
+
|
|
121
|
+
#### `VerifyOptions`
|
|
122
|
+
|
|
123
|
+
`{ field: DigestValueField }`.
|
|
124
|
+
|
|
125
|
+
## Pattern: Signing-Ready Outbound Request
|
|
126
|
+
|
|
127
|
+
Attach both fields so RFC 9421 and draft-cavage verifiers each find the one their signature covers.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { digest, stringify } from "@sdxc/digest-fields";
|
|
131
|
+
import { unwrap } from "@sdxc/result";
|
|
132
|
+
|
|
133
|
+
let body = new TextEncoder().encode(JSON.stringify({ type: "Create" }));
|
|
134
|
+
let digests = { "sha-256": unwrap(await digest(body, "sha-256")) };
|
|
135
|
+
|
|
136
|
+
let request = new Request("https://inbox.example.com/", {
|
|
137
|
+
method: "POST",
|
|
138
|
+
body,
|
|
139
|
+
headers: {
|
|
140
|
+
"content-type": "application/json",
|
|
141
|
+
"content-digest": unwrap(stringify(digests, "content-digest")),
|
|
142
|
+
digest: unwrap(stringify(digests, "digest")),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Pattern: Verifying Whichever Field the Sender Used
|
|
148
|
+
|
|
149
|
+
Check `Content-Digest` first, fall back to `Digest`, and map each failure to a status.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { verify } from "@sdxc/digest-fields";
|
|
153
|
+
import { isFailure } from "@sdxc/result";
|
|
154
|
+
|
|
155
|
+
async function checkBody(request: Request) {
|
|
156
|
+
let body = await request.bytes();
|
|
157
|
+
let field = request.headers.has("content-digest") ? "content-digest" : "digest";
|
|
158
|
+
let checked = await verify(request.headers, body, { field });
|
|
159
|
+
if (!isFailure(checked)) return body;
|
|
160
|
+
|
|
161
|
+
switch (checked.error.code) {
|
|
162
|
+
case "mismatch":
|
|
163
|
+
return new Response("Body does not match its digest", { status: 422 });
|
|
164
|
+
case "crypto":
|
|
165
|
+
return new Response(null, { status: 500 });
|
|
166
|
+
default:
|
|
167
|
+
return new Response(checked.error.message, { status: 400 });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
98
172
|
## Pattern: Answering `Want-Content-Digest`
|
|
99
173
|
|
|
100
174
|
Send the algorithm the client weighs highest, falling back to `sha-256`.
|
|
@@ -112,13 +186,15 @@ function preferred(request: Request) {
|
|
|
112
186
|
return ranked[0] ?? "sha-256";
|
|
113
187
|
}
|
|
114
188
|
|
|
115
|
-
|
|
116
|
-
let
|
|
117
|
-
let
|
|
189
|
+
async function respond(request: Request, data: unknown) {
|
|
190
|
+
let body = new TextEncoder().encode(JSON.stringify(data));
|
|
191
|
+
let algorithm = preferred(request);
|
|
192
|
+
let bytes = unwrap(await digest(body, algorithm));
|
|
118
193
|
|
|
119
|
-
return new Response(body, {
|
|
120
|
-
|
|
121
|
-
});
|
|
194
|
+
return new Response(body, {
|
|
195
|
+
headers: { "content-digest": unwrap(stringify({ [algorithm]: bytes }, "content-digest")) },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
122
198
|
```
|
|
123
199
|
|
|
124
200
|
## Versioning
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/digest-fields",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.13",
|
|
4
4
|
"description": "RFC 9530 Content-Digest and Repr-Digest, plus the RFC 3230 Digest header",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
".": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@sdxc/crypto": "2026.10.
|
|
12
|
-
"@sdxc/result": "2026.10.
|
|
13
|
-
"@sdxc/structured-fields": "2026.10.
|
|
11
|
+
"@sdxc/crypto": "2026.10.13",
|
|
12
|
+
"@sdxc/result": "2026.10.13",
|
|
13
|
+
"@sdxc/structured-fields": "2026.10.13"
|
|
14
14
|
},
|
|
15
|
-
"gitHead": "
|
|
15
|
+
"gitHead": "5ff758a57e35480a8db7abd24f1d9527b81bfb51",
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|