@sdxc/structured-fields 0.0.0-pre.1

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergio Xalambrí
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # @sdxc/structured-fields
2
+
3
+ Parse and serialize RFC 9651 structured HTTP field values.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm add @sdxc/structured-fields
9
+ ```
10
+
11
+ Results are [`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result) values, and the
12
+ `./schema` builders compose with [`remix`](https://www.npmjs.com/package/remix)'s
13
+ `remix/data-schema`. Both install alongside this package.
14
+
15
+ [RFC 9651](https://www.rfc-editor.org/rfc/rfc9651) defines one grammar for new HTTP fields:
16
+ `Priority`, `Cache-Status`, `RateLimit`, `Idempotency-Key`, `Repr-Digest` and the client hints
17
+ are all a List, a Dictionary or an Item of typed bare values. This package implements the RFC's
18
+ parsing and serialization algorithms, checked against the HTTP working group's
19
+ [structured-field-tests](https://github.com/httpwg/structured-field-tests) suite. Every call
20
+ names the field's top-level type (`"list"`, `"dictionary"` or `"item"`), because the RFC fixes
21
+ it in the field's definition rather than in the text. Parsing fails the whole field on any
22
+ error, as the RFC requires, and never returns a partial value.
23
+
24
+ ## Usage
25
+
26
+ ### Write A Field
27
+
28
+ ```typescript
29
+ import { setField, stringify } from "@sdxc/structured-fields";
30
+
31
+ setField(headers, "RateLimit", { limit: 10, remaining: 0, reset: 7 }, "dictionary");
32
+ // RateLimit: limit=10, remaining=0, reset=7
33
+
34
+ stringify({ value: 10, params: { w: 60 } }, "item"); // success: "10;w=60"
35
+ ```
36
+
37
+ ### Read The Untyped Model
38
+
39
+ ```typescript
40
+ import { parse } from "@sdxc/structured-fields";
41
+
42
+ let parsed = parse('ExampleCache; hit; ttl=376, "Origin"; fwd=uri-miss', "list");
43
+ // success: [
44
+ // { value: Token("ExampleCache"), params: { hit: true, ttl: 376 } },
45
+ // { value: "Origin", params: { fwd: Token("uri-miss") } },
46
+ // ]
47
+ ```
48
+
49
+ ### Read A Field Through A Schema
50
+
51
+ ```typescript
52
+ import { isSuccess } from "@sdxc/result";
53
+ import { getField } from "@sdxc/structured-fields";
54
+ import { sf } from "@sdxc/structured-fields/schema";
55
+ import * as s from "remix/data-schema";
56
+ import * as checks from "remix/data-schema/checks";
57
+
58
+ const PRIORITY = s.object({
59
+ u: s.optional(sf.value(sf.integer().pipe(checks.min(0), checks.max(7)))),
60
+ i: s.optional(sf.value(s.boolean())),
61
+ });
62
+
63
+ let priority = getField(request.headers, "Priority", "dictionary", PRIORITY);
64
+ if (isSuccess(priority) && priority.data) {
65
+ priority.data.u; // number | undefined
66
+ }
67
+ ```
68
+
69
+ `sf.*` schemas hold no state, so declare a field's schema once at module scope.
70
+
71
+ ## API
72
+
73
+ ### `@sdxc/structured-fields`
74
+
75
+ #### `parse(text, type, schema?)`
76
+
77
+ Parses a field value as `type`. Without a schema it returns the model (`SF.ValueOf[type]`) or a
78
+ `StructuredFieldParseError`. With a synchronous Standard Schema it validates the model and
79
+ returns the schema's output, or a `ValidationError` carrying the issues. Several field lines
80
+ joined with `,` (what `Headers#get` returns) are valid input.
81
+
82
+ ```typescript
83
+ parse("u=1, i", "dictionary"); // success: { u: { value: 1, params: {} }, i: { value: true, params: {} } }
84
+ parse("1, 2,", "list"); // failure: StructuredFieldParseError, position 5
85
+ ```
86
+
87
+ #### `stringify(value, type)`
88
+
89
+ Serializes to the canonical text, or fails with a `StructuredFieldStringifyError` whose `path`
90
+ names the offending value. An empty List or Dictionary succeeds with `""`. Members may be
91
+ written in their shorthand forms: a bare value for a member without parameters, `params`
92
+ omitted anywhere.
93
+
94
+ - A `number` that is an integer writes as an Integer (at most 15 digits), any other `number`
95
+ as a Decimal rounded half-to-even to three fractional digits
96
+ - A `string` writes as a quoted sf-string, which carries printable ASCII only
97
+ - A member or parameter whose value is `true` writes as the bare key
98
+ - A `Date` writes as whole seconds; one with milliseconds fails
99
+
100
+ ```typescript
101
+ stringify([1, 1.5, new Decimal(1)], "list"); // success: "1, 1.5, 1.0"
102
+ stringify({ i: true, u: 3 }, "dictionary"); // success: "i, u=3"
103
+ stringify("fü", "item"); // failure: use new DisplayString("fü")
104
+ ```
105
+
106
+ #### `getField(headers, name, type, schema?)`
107
+
108
+ Reads a field from `Headers` through `parse`. An absent field succeeds with `null`.
109
+
110
+ #### `setField(headers, name, value, type)`
111
+
112
+ Writes a field onto `Headers` through `stringify`, replacing any earlier value. An empty List or
113
+ Dictionary deletes the field. A failure leaves `headers` untouched.
114
+
115
+ #### `Token`, `Decimal`, `DisplayString`
116
+
117
+ Wrappers, each holding a readonly `value`, for the bare item types a JavaScript primitive cannot
118
+ tell apart: `new Token("HIT")` writes `HIT` where `"HIT"` writes `"HIT"`, `new Decimal(1)`
119
+ writes `1.0`, and `new DisplayString("fü")` writes `%"f%c3%bc"`. `parse` returns them, so a
120
+ round trip is lossless. Byte Sequences are `Uint8Array`, Dates are `Date`, Booleans are
121
+ `boolean`.
122
+
123
+ #### `StructuredFieldParseError`
124
+
125
+ `position` is the offset into the field value where parsing stopped. RFC 9651 has recipients
126
+ ignore an invalid field as a whole, so treat this failure as an absent field.
127
+
128
+ #### `StructuredFieldStringifyError`
129
+
130
+ `path` lists the keys and indices leading to the value with no representation, such as
131
+ `[0, "params", "q"]`.
132
+
133
+ #### `ValidationError`
134
+
135
+ `remix/data-schema`'s error, re-exported so a caller branches on both failures from one import.
136
+ `issues` holds the Standard Schema issues.
137
+
138
+ #### `SF` (types)
139
+
140
+ | Type | Meaning |
141
+ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
142
+ | `SF.BareItem` | `number \| Decimal \| string \| Token \| Uint8Array \| boolean \| Date \| DisplayString` |
143
+ | `SF.Parameters` | `Record<string, BareItem>` on a null-prototype object, in field order |
144
+ | `SF.Item` | `{ value, params }` |
145
+ | `SF.InnerList` | `{ items: Item[], params }` |
146
+ | `SF.Member`, `SF.List`, `SF.Dictionary` | A List or Dictionary member, the List, and the Dictionary (null prototype) |
147
+ | `SF.FieldType` | `"list" \| "dictionary" \| "item"` |
148
+ | `SF.ValueOf`, `SF.InputOf` | What `parse` returns and what `stringify` accepts, per field type |
149
+ | `SF.ItemInput`, `SF.InnerListInput`, `SF.MemberInput` | The shorthand member forms `stringify` accepts |
150
+ | `SF.SyncSchema<Output>` | A Standard Schema whose `validate` answers synchronously |
151
+
152
+ ### `@sdxc/structured-fields/schema`
153
+
154
+ #### `sf`
155
+
156
+ Schema builders made with `remix/data-schema`'s `createSchema`, so they compose with
157
+ `s.object`, `s.array`, `s.union`, `s.optional`, `.pipe()` and `.refine()`. Issues point at
158
+ their place in the field, such as `["u", "value"]` or `[0, "items", 1, "value"]`, and carry an
159
+ error-map code (`sf.integer`, `sf.token.allowed`, …) for localized messages.
160
+ `StructuredFieldSchemas` is its type.
161
+
162
+ | Builder | Accepts | Outputs |
163
+ | ----------------------------- | --------------------------------- | ---------------------------------- |
164
+ | `sf.integer()` | An Integer within 15 digits | `number` |
165
+ | `sf.decimal()` | A Decimal or an Integer | `number` |
166
+ | `sf.token(allowed?)` | A Token, optionally one of a list | its text, narrowed to the literals |
167
+ | `sf.displayString()` | A Display String | `string` |
168
+ | `sf.bytes()` | A Byte Sequence | `Uint8Array` |
169
+ | `sf.date()` | A Date | `Date` |
170
+ | `sf.item(value, params?)` | An Item | `{ value, params }` |
171
+ | `sf.value(value)` | An Item, parameters ignored | the bare value's output |
172
+ | `sf.innerList(item, params?)` | An Inner List | `{ items, params }` |
173
+
174
+ A plain sf-string and a Boolean need no helper: `s.string()` and `s.boolean()`. Parameters and
175
+ Dictionaries are `s.object(...)`, with `s.optional` for keys a field allows but does not
176
+ require; the object schema's `unknownKeys` option decides what happens to the rest.
177
+
178
+ ## Pattern: Read RFC 9211 Cache-Status
179
+
180
+ A List of cache hops, each a Token or String naming the cache, with typed parameters.
181
+
182
+ ```typescript
183
+ import { getField } from "@sdxc/structured-fields";
184
+ import { sf } from "@sdxc/structured-fields/schema";
185
+ import * as s from "remix/data-schema";
186
+
187
+ const HOP = sf.item(
188
+ s.union([sf.token(), s.string()]),
189
+ s.object({
190
+ hit: s.optional(s.boolean()),
191
+ fwd: s.optional(
192
+ sf.token([
193
+ "bypass",
194
+ "method",
195
+ "uri-miss",
196
+ "vary-miss",
197
+ "miss",
198
+ "request",
199
+ "stale",
200
+ "partial",
201
+ ]),
202
+ ),
203
+ ttl: s.optional(sf.integer()),
204
+ stored: s.optional(s.boolean()),
205
+ }),
206
+ );
207
+
208
+ let hops = getField(response.headers, "Cache-Status", "list", s.array(HOP));
209
+ ```
210
+
211
+ ## Pattern: An Idempotency-Key Item
212
+
213
+ The field is an Item whose value is an sf-string. Reading unwraps it; writing quotes and escapes
214
+ it.
215
+
216
+ ```typescript
217
+ import { getField, stringify } from "@sdxc/structured-fields";
218
+ import { sf } from "@sdxc/structured-fields/schema";
219
+ import * as s from "remix/data-schema";
220
+
221
+ let key = getField(request.headers, "Idempotency-Key", "item", sf.value(s.string()));
222
+ let header = stringify("8e03978e-40d5-43e8-bc93-6894a57f9324", "item");
223
+ // success: "\"8e03978e-40d5-43e8-bc93-6894a57f9324\""
224
+ ```
225
+
226
+ ## Pattern: Write Only The Members You Have
227
+
228
+ `setField` deletes the field when the Dictionary ends up empty, so build the object from the
229
+ values that exist and write it unconditionally.
230
+
231
+ ```typescript
232
+ import { setField } from "@sdxc/structured-fields";
233
+
234
+ let quota: Record<string, number> = {};
235
+ if (limit !== null) quota.limit = limit;
236
+ if (remaining !== null) quota.remaining = remaining;
237
+
238
+ let written = setField(headers, "RateLimit", quota, "dictionary");
239
+ ```
240
+
241
+ ## Versioning
242
+
243
+ Releases are dated rather than semantic. A version is the UTC date it was published,
244
+ written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one
245
+ release goes out per day.
246
+
247
+ Those numbers say when, not what: a later date means a later release and carries no
248
+ compatibility promise. Any release may change or remove an export.
249
+
250
+ Depend on one exact date, and move it when you are ready to take the change:
251
+
252
+ ```json
253
+ {
254
+ "dependencies": {
255
+ "@sdxc/structured-fields": "2026.9.4"
256
+ }
257
+ }
258
+ ```
259
+
260
+ A caret or tilde range reads the date as major, minor and patch, so it accepts every
261
+ later release in the same year. An exact version keeps the upgrade yours to schedule.
262
+
263
+ ## License
264
+
265
+ MIT
266
+
267
+ ## Author
268
+
269
+ [Sergio Xalambrí](https://sergiodxa.com)
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Parses and serializes RFC 9651 Structured Field Values: the grammar new HTTP fields such
3
+ * as `Priority`, `Cache-Status` and `Idempotency-Key` are written against, with a value
4
+ * model that round-trips, typed parsing through a schema, and `Headers` helpers.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
11
+ import { ValidationError } from "remix/data-schema";
12
+ import type { SF } from "./lib/types.js";
13
+ import { StructuredFieldParseError, StructuredFieldStringifyError } from "./lib/errors.js";
14
+ export type { SF } from "./lib/types.js";
15
+ export { StructuredFieldParseError, StructuredFieldStringifyError } from "./lib/errors.js";
16
+ export { Decimal, DisplayString, Token } from "./lib/values.js";
17
+ export { ValidationError };
18
+ /**
19
+ * Parses a field value into the untyped model. Any error fails the whole field, as RFC 9651
20
+ * requires, so a failure never carries a partial value.
21
+ *
22
+ * @param text - The field value; several field lines joined with `,` (as `Headers#get` does)
23
+ * @param type - The top-level type the field's definition fixes
24
+ * @returns The model, or where the text stops being valid
25
+ * @example parse("u=1, i", "dictionary")
26
+ */
27
+ export declare function parse<Type extends SF.FieldType>(text: string, type: Type): Result<SF.ValueOf[Type], StructuredFieldParseError>;
28
+ /**
29
+ * Parses a field value, then validates the model against a schema, so the caller receives
30
+ * the schema's typed output or a failure naming why the field does not fit it.
31
+ *
32
+ * @param text - The field value
33
+ * @param type - The top-level type the field's definition fixes
34
+ * @param schema - A synchronous schema over the model, such as one built with `sf.*`
35
+ * @returns The schema's output, or a parse or validation failure
36
+ */
37
+ export declare function parse<Type extends SF.FieldType, Schema extends SF.SyncSchema>(text: string, type: Type, schema: Schema): Result<StandardSchemaV1.InferOutput<Schema>, StructuredFieldParseError | ValidationError>;
38
+ /**
39
+ * Serializes a value to its canonical field text. An empty List or Dictionary succeeds with
40
+ * `""`, which RFC 9651 reads as "do not send the field".
41
+ *
42
+ * @param value - The model, or any member written as its bare value
43
+ * @param type - The top-level type the field's definition fixes; a Dictionary keyed
44
+ * `value` and `params` looks like an Item, so the value alone cannot say which to write
45
+ * @returns The text, or the path to the value with no RFC 9651 representation
46
+ * @example stringify({ value: 10, params: { w: 60 } }, "item") // "10;w=60"
47
+ */
48
+ export declare function stringify<Type extends SF.FieldType>(value: SF.InputOf[Type], type: Type): Result<string, StructuredFieldStringifyError>;
49
+ /**
50
+ * Reads a field from `Headers`. Repeated field lines arrive joined with `, `, which is how
51
+ * RFC 9651 combines them.
52
+ *
53
+ * @param headers - The request or response headers
54
+ * @param name - The field name
55
+ * @param type - The top-level type the field's definition fixes
56
+ * @returns The model, `null` when the field is absent, or why it is invalid
57
+ */
58
+ export declare function getField<Type extends SF.FieldType>(headers: Headers, name: string, type: Type): Result<SF.ValueOf[Type] | null, StructuredFieldParseError>;
59
+ /**
60
+ * Reads a field from `Headers` and validates it against a schema.
61
+ *
62
+ * @param headers - The request or response headers
63
+ * @param name - The field name
64
+ * @param type - The top-level type the field's definition fixes
65
+ * @param schema - A synchronous schema over the model
66
+ * @returns The schema's output, `null` when the field is absent, or why it does not fit
67
+ */
68
+ export declare function getField<Type extends SF.FieldType, Schema extends SF.SyncSchema>(headers: Headers, name: string, type: Type, schema: Schema): Result<StandardSchemaV1.InferOutput<Schema> | null, StructuredFieldParseError | ValidationError>;
69
+ /**
70
+ * Writes a field onto `Headers`, replacing any earlier value. An empty List or Dictionary
71
+ * deletes the field instead, since RFC 9651 treats an empty one as absent.
72
+ *
73
+ * @param headers - The headers to write
74
+ * @param name - The field name
75
+ * @param value - The model, or any member written as its bare value
76
+ * @param type - The top-level type the field's definition fixes
77
+ * @returns Success once written, or why the value has no representation, leaving
78
+ * `headers` untouched
79
+ */
80
+ export declare function setField<Type extends SF.FieldType>(headers: Headers, name: string, value: SF.InputOf[Type], type: Type): Result<void, StructuredFieldStringifyError>;
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ import { failure, isFailure, success } from "@sdxc/result";
2
+ import { ValidationError } from "remix/data-schema";
3
+ import { StructuredFieldParseError, StructuredFieldStringifyError } from "./lib/errors.js";
4
+ import { parseField } from "./lib/parse.js";
5
+ import { stringifyField } from "./lib/stringify.js";
6
+ export { StructuredFieldParseError, StructuredFieldStringifyError } from "./lib/errors.js";
7
+ export { Decimal, DisplayString, Token } from "./lib/values.js";
8
+ export { ValidationError };
9
+ export function parse(text, type, schema) {
10
+ let parsed = parseField(text, type);
11
+ if (isFailure(parsed) || schema === undefined)
12
+ return parsed;
13
+ return validateModel(parsed.data, schema);
14
+ }
15
+ /**
16
+ * Runs a schema over a parsed model. A schema that answers with a Promise despite its type
17
+ * fails validation, keeping every parse synchronous.
18
+ *
19
+ * @param model - The parsed field
20
+ * @param schema - The caller's schema
21
+ * @returns The schema's output, or its issues
22
+ */
23
+ function validateModel(model, schema) {
24
+ let result = schema["~standard"].validate(model);
25
+ if (result instanceof Promise) {
26
+ return failure(new ValidationError([{ message: "Schema must validate synchronously" }]));
27
+ }
28
+ if (result.issues)
29
+ return failure(new ValidationError(result.issues));
30
+ return success(result.value);
31
+ }
32
+ /**
33
+ * Serializes a value to its canonical field text. An empty List or Dictionary succeeds with
34
+ * `""`, which RFC 9651 reads as "do not send the field".
35
+ *
36
+ * @param value - The model, or any member written as its bare value
37
+ * @param type - The top-level type the field's definition fixes; a Dictionary keyed
38
+ * `value` and `params` looks like an Item, so the value alone cannot say which to write
39
+ * @returns The text, or the path to the value with no RFC 9651 representation
40
+ * @example stringify({ value: 10, params: { w: 60 } }, "item") // "10;w=60"
41
+ */
42
+ export function stringify(value, type) {
43
+ return stringifyField(value, type);
44
+ }
45
+ export function getField(headers, name, type, schema) {
46
+ let text = headers.get(name);
47
+ if (text === null)
48
+ return success(null);
49
+ return schema === undefined ? parse(text, type) : parse(text, type, schema);
50
+ }
51
+ /**
52
+ * Writes a field onto `Headers`, replacing any earlier value. An empty List or Dictionary
53
+ * deletes the field instead, since RFC 9651 treats an empty one as absent.
54
+ *
55
+ * @param headers - The headers to write
56
+ * @param name - The field name
57
+ * @param value - The model, or any member written as its bare value
58
+ * @param type - The top-level type the field's definition fixes
59
+ * @returns Success once written, or why the value has no representation, leaving
60
+ * `headers` untouched
61
+ */
62
+ export function setField(headers, name, value, type) {
63
+ let text = stringifyField(value, type);
64
+ if (isFailure(text))
65
+ return text;
66
+ if (text.data === "")
67
+ headers.delete(name);
68
+ else
69
+ headers.set(name, text.data);
70
+ return success(undefined);
71
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Base64 for Byte Sequences over the platform's `atob` and `btoa`, which every runtime the
3
+ * package targets provides. Decoding follows the forgiving base64 algorithm, so missing
4
+ * padding and nonzero pad bits are accepted, as RFC 9651 section 4.2.7 asks of parsers.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Encodes bytes as padded standard base64, the canonical form RFC 9651 serializes.
11
+ *
12
+ * @param bytes - The Byte Sequence
13
+ * @returns Its base64 text
14
+ */
15
+ export declare function encodeBase64(bytes: Uint8Array): string;
16
+ /**
17
+ * Decodes standard base64 text whose characters the caller already restricted to the
18
+ * base64 alphabet and `=`.
19
+ *
20
+ * @param text - The text between a Byte Sequence's colons
21
+ * @returns The bytes, or `null` when padding sits anywhere but the end or the length
22
+ * leaves a lone trailing character
23
+ */
24
+ export declare function decodeBase64(text: string): Uint8Array | null;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Base64 for Byte Sequences over the platform's `atob` and `btoa`, which every runtime the
3
+ * package targets provides. Decoding follows the forgiving base64 algorithm, so missing
4
+ * padding and nonzero pad bits are accepted, as RFC 9651 section 4.2.7 asks of parsers.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /** Bytes converted per `String.fromCharCode` call, well under engines' argument limits. */
10
+ const CHUNK_SIZE = 0x8000;
11
+ /**
12
+ * Encodes bytes as padded standard base64, the canonical form RFC 9651 serializes.
13
+ *
14
+ * @param bytes - The Byte Sequence
15
+ * @returns Its base64 text
16
+ */
17
+ export function encodeBase64(bytes) {
18
+ let binary = "";
19
+ for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE) {
20
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + CHUNK_SIZE));
21
+ }
22
+ return btoa(binary);
23
+ }
24
+ /**
25
+ * Decodes standard base64 text whose characters the caller already restricted to the
26
+ * base64 alphabet and `=`.
27
+ *
28
+ * @param text - The text between a Byte Sequence's colons
29
+ * @returns The bytes, or `null` when padding sits anywhere but the end or the length
30
+ * leaves a lone trailing character
31
+ */
32
+ export function decodeBase64(text) {
33
+ let binary;
34
+ try {
35
+ binary = atob(text);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ let bytes = new Uint8Array(binary.length);
41
+ for (let index = 0; index < binary.length; index++)
42
+ bytes[index] = binary.charCodeAt(index);
43
+ return bytes;
44
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The two failures the package reports, kept apart from the entry point so the parser
3
+ * and the serializer construct them without importing each other.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ /**
9
+ * The text is not a valid Structured Field of the requested type. RFC 9651 has the
10
+ * recipient ignore such a field as a whole, so a failure never carries a partial value.
11
+ */
12
+ export declare class StructuredFieldParseError extends Error {
13
+ name: "StructuredFieldParseError";
14
+ /** Offset into the field value where parsing stopped, counting from 0. */
15
+ readonly position: number;
16
+ /**
17
+ * @param message - Human readable description of the failure
18
+ * @param position - Offset into the field value where parsing stopped
19
+ */
20
+ constructor(message: string, position: number);
21
+ }
22
+ /**
23
+ * A value has no RFC 9651 representation: a number out of range, a character outside a
24
+ * type's set, a key that breaks the key grammar, or a Date carrying milliseconds.
25
+ */
26
+ export declare class StructuredFieldStringifyError extends Error {
27
+ name: "StructuredFieldStringifyError";
28
+ /** Where in the input the offending value sits, empty for the value itself. */
29
+ readonly path: Array<string | number>;
30
+ /**
31
+ * @param message - Human readable description of the failure
32
+ * @param path - Keys and indices leading to the offending value
33
+ */
34
+ constructor(message: string, path: Array<string | number>);
35
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The two failures the package reports, kept apart from the entry point so the parser
3
+ * and the serializer construct them without importing each other.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ /**
9
+ * The text is not a valid Structured Field of the requested type. RFC 9651 has the
10
+ * recipient ignore such a field as a whole, so a failure never carries a partial value.
11
+ */
12
+ export class StructuredFieldParseError extends Error {
13
+ name = "StructuredFieldParseError";
14
+ /** Offset into the field value where parsing stopped, counting from 0. */
15
+ position;
16
+ /**
17
+ * @param message - Human readable description of the failure
18
+ * @param position - Offset into the field value where parsing stopped
19
+ */
20
+ constructor(message, position) {
21
+ super(`${message} at position ${position}`);
22
+ this.position = position;
23
+ }
24
+ }
25
+ /**
26
+ * A value has no RFC 9651 representation: a number out of range, a character outside a
27
+ * type's set, a key that breaks the key grammar, or a Date carrying milliseconds.
28
+ */
29
+ export class StructuredFieldStringifyError extends Error {
30
+ name = "StructuredFieldStringifyError";
31
+ /** Where in the input the offending value sits, empty for the value itself. */
32
+ path;
33
+ /**
34
+ * @param message - Human readable description of the failure
35
+ * @param path - Keys and indices leading to the offending value
36
+ */
37
+ constructor(message, path) {
38
+ super(path.length === 0 ? message : `${message} at ${path.join(".")}`);
39
+ this.path = path;
40
+ }
41
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The RFC 9651 section 4.2 parsing algorithms, step by step: the three top-level types,
3
+ * inner lists, parameters, keys and the eight bare item types. Any failure fails the whole
4
+ * field, so each step either returns its value or the `FAILED` marker with the reason set.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { SF } from "./types.js";
11
+ import { StructuredFieldParseError } from "./errors.js";
12
+ /**
13
+ * Parses a field value as its definition's top-level type (RFC 9651 section 4.2).
14
+ *
15
+ * @param text - The field value, several field lines joined with `,`
16
+ * @param type - The top-level type the field's definition fixes
17
+ * @returns The model, or where the text stops being valid
18
+ */
19
+ export declare function parseField<Type extends SF.FieldType>(text: string, type: Type): Result<SF.ValueOf[Type], StructuredFieldParseError>;