@sdxc/digest-fields 2026.10.10 → 2026.10.16

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.
Files changed (2) hide show
  1. package/README.md +102 -26
  2. package/package.json +6 -6
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @sdxc/digest-fields
2
2
 
3
- RFC 9530 `Content-Digest`, `Repr-Digest` and their `Want-*` preferences, plus the RFC 3230 `Digest` header that draft-cavage HTTP signatures cover.
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, stringify, verify } from "@sdxc/digest-fields";
17
- import { isFailure, unwrap } from "@sdxc/result";
20
+ import { digest } from "@sdxc/digest-fields";
21
+ import { unwrap } from "@sdxc/result";
18
22
 
19
- let body = new TextEncoder().encode(JSON.stringify(activity));
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
- let headers = new Headers({
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
- let checked = await verify(request.headers, await request.bytes(), { field: "content-digest" });
28
- if (isFailure(checked)) return new Response(checked.error.message, { status: 400 });
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
- Reading a field into its value model:
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`. Text is read as UTF-8, so pass the exact bytes sent when the body is not UTF-8 text.
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 go through the Structured Field grammar of [`@sdxc/structured-fields`](https://www.npmjs.com/package/@sdxc/structured-fields). `Digest` accepts algorithm names in any case, and drops an entry for another algorithm whose value is not base64, such as `UNIXsum=30637`.
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=…`), which is how every draft-cavage verifier reads them. An empty value answers `""`, meaning "do not send the field".
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: { field: DigestValueField }): Promise<Result<void, DigestError>>`
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, and entries for other algorithms are skipped.
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. `code` says which check failed:
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 `Preferences`
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
- let body = new TextEncoder().encode(JSON.stringify(data));
116
- let algorithm = preferred(request);
117
- let bytes = unwrap(await digest(body, algorithm));
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
- headers: { "content-digest": unwrap(stringify({ [algorithm]: bytes }, "content-digest")) },
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.10",
3
+ "version": "2026.10.16",
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.10",
12
- "@sdxc/result": "2026.10.6",
13
- "@sdxc/structured-fields": "2026.10.6"
11
+ "@sdxc/crypto": "2026.10.13",
12
+ "@sdxc/result": "2026.10.13",
13
+ "@sdxc/structured-fields": "2026.10.16"
14
14
  },
15
- "gitHead": "b61f19c59866708a2d89a5a8f4655ebbc16941b0",
15
+ "gitHead": "1e547d8431568f5ed9bdb079ca3046c936240fde",
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
@@ -21,7 +21,7 @@
21
21
  "url": "git+https://github.com/sergiodxa/monorepo.git",
22
22
  "directory": "packages/digest-fields"
23
23
  },
24
- "homepage": "https://sdxc.sergiodxa.com/api/digest-fields",
24
+ "homepage": "https://sdxc.dev/api/digest-fields",
25
25
  "author": {
26
26
  "name": "Sergio Xalambrí",
27
27
  "url": "https://sergiodxa.com"