@sdxc/well-known 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 +21 -0
- package/README.md +313 -0
- package/dist/change-password.d.ts +19 -0
- package/dist/change-password.js +21 -0
- package/dist/format.d.ts +34 -0
- package/dist/format.js +0 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/jwks.d.ts +80 -0
- package/dist/jwks.js +100 -0
- package/dist/lib/identifier.d.ts +15 -0
- package/dist/lib/identifier.js +32 -0
- package/dist/lib/json.d.ts +45 -0
- package/dist/lib/json.js +67 -0
- package/dist/lib/metadata.d.ts +66 -0
- package/dist/lib/metadata.js +242 -0
- package/dist/lib/oauth-fields.d.ts +291 -0
- package/dist/lib/oauth-fields.js +103 -0
- package/dist/middleware.d.ts +35 -0
- package/dist/middleware.js +88 -0
- package/dist/oauth-authorization-server.d.ts +99 -0
- package/dist/oauth-authorization-server.js +61 -0
- package/dist/oauth-protected-resource.d.ts +110 -0
- package/dist/oauth-protected-resource.js +136 -0
- package/dist/openid-configuration.d.ts +75 -0
- package/dist/openid-configuration.js +60 -0
- package/dist/parse-error.d.ts +34 -0
- package/dist/parse-error.js +29 -0
- package/dist/passkey-endpoints.d.ts +36 -0
- package/dist/passkey-endpoints.js +40 -0
- package/dist/response.d.ts +32 -0
- package/dist/response.js +35 -0
- package/dist/security-txt.d.ts +64 -0
- package/dist/security-txt.js +228 -0
- package/dist/webfinger.d.ts +75 -0
- package/dist/webfinger.js +205 -0
- package/dist/well-known-url.d.ts +26 -0
- package/dist/well-known-url.js +29 -0
- package/package.json +46 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON side every JSON well-known reader shares: decoding text into an object,
|
|
3
|
+
* JSON Pointers for issues, and validating extension members through a Standard
|
|
4
|
+
* Schema, so each document reports its failures in the same shape.
|
|
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 { WellKnownParseError } from "../parse-error.js";
|
|
12
|
+
/** A decoded JSON object, read member by member. */
|
|
13
|
+
export type JsonObject = Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* A JSON Pointer (RFC 6901) to a member, escaping `~` and `/` so a member name holding
|
|
16
|
+
* either still points at itself.
|
|
17
|
+
*
|
|
18
|
+
* @param segments - Member names and array indexes, outermost first.
|
|
19
|
+
*/
|
|
20
|
+
export declare function pointer(...segments: Array<string | number>): string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a decoded value is a plain JSON object.
|
|
23
|
+
*
|
|
24
|
+
* @param value - Any decoded JSON value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isObject(value: unknown): value is JsonObject;
|
|
27
|
+
/**
|
|
28
|
+
* Decodes a document's text, which every JSON well-known document requires to be an
|
|
29
|
+
* object at the top level.
|
|
30
|
+
*
|
|
31
|
+
* @param text - The served text.
|
|
32
|
+
* @param format - The registered name, for the error.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readJsonObject(text: string, format: string): Result<JsonObject, WellKnownParseError>;
|
|
35
|
+
/**
|
|
36
|
+
* Validates extension members through a schema, mapping its issues to JSON Pointers
|
|
37
|
+
* into the document. A schema must validate synchronously, since `parse` returns a
|
|
38
|
+
* `Result` rather than a promise.
|
|
39
|
+
*
|
|
40
|
+
* @param members - The members outside the specification.
|
|
41
|
+
* @param schema - The schema, or `undefined` to keep the members unchecked.
|
|
42
|
+
* @param issues - Where schema issues are collected.
|
|
43
|
+
* @returns The validated extensions, or `null` when the schema rejected them.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readExtensions<Extensions extends object>(members: JsonObject, schema: StandardSchemaV1<unknown, Extensions> | undefined, issues: WellKnownParseError.Issue[]): Extensions | null;
|
package/dist/lib/json.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { failure, success } from "@sdxc/result";
|
|
2
|
+
import { WellKnownParseError } from "../parse-error.js";
|
|
3
|
+
/**
|
|
4
|
+
* A JSON Pointer (RFC 6901) to a member, escaping `~` and `/` so a member name holding
|
|
5
|
+
* either still points at itself.
|
|
6
|
+
*
|
|
7
|
+
* @param segments - Member names and array indexes, outermost first.
|
|
8
|
+
*/
|
|
9
|
+
export function pointer(...segments) {
|
|
10
|
+
return segments
|
|
11
|
+
.map((segment) => `/${String(segment).replace(/~/g, "~0").replace(/\//g, "~1")}`)
|
|
12
|
+
.join("");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether a decoded value is a plain JSON object.
|
|
16
|
+
*
|
|
17
|
+
* @param value - Any decoded JSON value.
|
|
18
|
+
*/
|
|
19
|
+
export function isObject(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Decodes a document's text, which every JSON well-known document requires to be an
|
|
24
|
+
* object at the top level.
|
|
25
|
+
*
|
|
26
|
+
* @param text - The served text.
|
|
27
|
+
* @param format - The registered name, for the error.
|
|
28
|
+
*/
|
|
29
|
+
export function readJsonObject(text, format) {
|
|
30
|
+
let json;
|
|
31
|
+
try {
|
|
32
|
+
json = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return failure(new WellKnownParseError(format, [{ at: "", message: "The document is not valid JSON." }]));
|
|
36
|
+
}
|
|
37
|
+
if (!isObject(json)) {
|
|
38
|
+
return failure(new WellKnownParseError(format, [{ at: "", message: "The document is not a JSON object." }]));
|
|
39
|
+
}
|
|
40
|
+
return success(json);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validates extension members through a schema, mapping its issues to JSON Pointers
|
|
44
|
+
* into the document. A schema must validate synchronously, since `parse` returns a
|
|
45
|
+
* `Result` rather than a promise.
|
|
46
|
+
*
|
|
47
|
+
* @param members - The members outside the specification.
|
|
48
|
+
* @param schema - The schema, or `undefined` to keep the members unchecked.
|
|
49
|
+
* @param issues - Where schema issues are collected.
|
|
50
|
+
* @returns The validated extensions, or `null` when the schema rejected them.
|
|
51
|
+
*/
|
|
52
|
+
export function readExtensions(members, schema, issues) {
|
|
53
|
+
if (schema === undefined)
|
|
54
|
+
return members;
|
|
55
|
+
let result = schema["~standard"].validate(members);
|
|
56
|
+
if (result instanceof Promise) {
|
|
57
|
+
issues.push({ at: "", message: "The extension schema must validate synchronously." });
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (result.issues === undefined)
|
|
61
|
+
return result.value;
|
|
62
|
+
for (let issue of result.issues) {
|
|
63
|
+
let path = (issue.path ?? []).map((segment) => typeof segment === "object" ? String(segment.key) : String(segment));
|
|
64
|
+
issues.push({ at: pointer(...path), message: issue.message });
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The field-mapping engine behind the OAuth metadata documents (RFC 8414, OpenID
|
|
3
|
+
* Connect Discovery, RFC 9728): one table maps each camelCase field to its registered
|
|
4
|
+
* member name and kind, and drives reading, writing and defaults from that one place.
|
|
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 { WellKnownParseError } from "../parse-error.js";
|
|
12
|
+
/**
|
|
13
|
+
* How a member's JSON value maps to its field: `text` is a string, `url` one absolute
|
|
14
|
+
* URL, `urls` and `strings` lists, `boolean` a flag, and `localized` a human-readable
|
|
15
|
+
* string with `#`-tagged language variants (RFC 9728 §2.1).
|
|
16
|
+
*/
|
|
17
|
+
export type FieldKind = "text" | "url" | "urls" | "strings" | "boolean" | "localized";
|
|
18
|
+
/** One row of the mapping table. */
|
|
19
|
+
export interface Field {
|
|
20
|
+
/** The member name the specification registers. */
|
|
21
|
+
wire: string;
|
|
22
|
+
kind: FieldKind;
|
|
23
|
+
/** A missing member fails the parse instead of taking the default. */
|
|
24
|
+
required?: boolean;
|
|
25
|
+
/** The value an absent boolean member means, which `stringify` leaves unwritten. */
|
|
26
|
+
default?: boolean;
|
|
27
|
+
/** The only values a `strings` member may list. */
|
|
28
|
+
values?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/** A document's mapping table, keyed by camelCase field name. */
|
|
31
|
+
export type FieldTable = Record<string, Field>;
|
|
32
|
+
/**
|
|
33
|
+
* Every field of a table at its default, for `define` to lay the app's values over.
|
|
34
|
+
*
|
|
35
|
+
* @param table - The document's mapping table.
|
|
36
|
+
*/
|
|
37
|
+
export declare function defaults(table: FieldTable): Record<string, unknown>;
|
|
38
|
+
/** How a metadata document is read. */
|
|
39
|
+
export interface ReadOptions<Extensions extends object> {
|
|
40
|
+
format: string;
|
|
41
|
+
table: FieldTable;
|
|
42
|
+
extensions?: StandardSchemaV1<unknown, Extensions>;
|
|
43
|
+
/** Checks the read fields against what the caller expected, adding issues on a mismatch. */
|
|
44
|
+
check?: (fields: Record<string, unknown>, issues: WellKnownParseError.Issue[]) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reads a metadata document through its table. Members outside the table become the
|
|
48
|
+
* extensions, validated by the schema when one is given. Every issue is collected
|
|
49
|
+
* before failing, so the error names all of them.
|
|
50
|
+
*
|
|
51
|
+
* @param text - The served JSON.
|
|
52
|
+
* @param options - The table, the extension schema and the identity check.
|
|
53
|
+
* @template Extensions - The extension members the schema produces.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readMetadata<Extensions extends object>(text: string, options: ReadOptions<Extensions>): Result<Record<string, unknown> & {
|
|
56
|
+
extensions: Extensions;
|
|
57
|
+
}, WellKnownParseError>;
|
|
58
|
+
/**
|
|
59
|
+
* Writes a document through its table, omitting `null` members, optional empty lists and
|
|
60
|
+
* default flags; a required list is written even when empty so the output parses back.
|
|
61
|
+
* Extensions never replace a standard member: any member name the table registers is dropped.
|
|
62
|
+
*
|
|
63
|
+
* @param document - The typed document.
|
|
64
|
+
* @param table - The document's mapping table.
|
|
65
|
+
*/
|
|
66
|
+
export declare function writeMetadata(document: object, table: FieldTable): string;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { failure, success } from "@sdxc/result";
|
|
2
|
+
import { WellKnownParseError } from "../parse-error.js";
|
|
3
|
+
import { pointer, readExtensions, readJsonObject } from "./json.js";
|
|
4
|
+
/**
|
|
5
|
+
* The value an absent member takes: `null` for single values, an empty list for
|
|
6
|
+
* lists, and the specification's default for flags.
|
|
7
|
+
*
|
|
8
|
+
* @param field - The row to default.
|
|
9
|
+
*/
|
|
10
|
+
function defaultOf(field) {
|
|
11
|
+
if (field.kind === "urls" || field.kind === "strings")
|
|
12
|
+
return [];
|
|
13
|
+
if (field.kind === "boolean")
|
|
14
|
+
return field.default ?? false;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Every field of a table at its default, for `define` to lay the app's values over.
|
|
19
|
+
*
|
|
20
|
+
* @param table - The document's mapping table.
|
|
21
|
+
*/
|
|
22
|
+
export function defaults(table) {
|
|
23
|
+
let result = {};
|
|
24
|
+
for (let [name, field] of Object.entries(table))
|
|
25
|
+
result[name] = defaultOf(field);
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether a member name is one the table registers, counting the `#`-tagged variants of
|
|
30
|
+
* a localized member, so extensions never shadow or duplicate a standard member.
|
|
31
|
+
*
|
|
32
|
+
* @param table - The document's mapping table.
|
|
33
|
+
* @param member - A member name from the document.
|
|
34
|
+
*/
|
|
35
|
+
function isStandardMember(table, member) {
|
|
36
|
+
let [base] = member.split("#");
|
|
37
|
+
for (let field of Object.values(table)) {
|
|
38
|
+
if (field.wire === member)
|
|
39
|
+
return true;
|
|
40
|
+
if (field.kind === "localized" && field.wire === base)
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a string is an absolute URL.
|
|
47
|
+
*
|
|
48
|
+
* @param value - Any decoded value.
|
|
49
|
+
*/
|
|
50
|
+
function isUrl(value) {
|
|
51
|
+
return typeof value === "string" && URL.canParse(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reads one member into its field's value, collecting an issue when the member has the
|
|
55
|
+
* wrong shape; the returned value is then meaningless and the parse fails.
|
|
56
|
+
*
|
|
57
|
+
* @param body - The decoded document.
|
|
58
|
+
* @param field - The row to read.
|
|
59
|
+
* @param issues - Where problems are collected.
|
|
60
|
+
*/
|
|
61
|
+
function readField(body, field, issues) {
|
|
62
|
+
let at = pointer(field.wire);
|
|
63
|
+
let value = body[field.wire];
|
|
64
|
+
if (field.kind === "localized")
|
|
65
|
+
return readLocalized(body, field, issues);
|
|
66
|
+
if (value === undefined || value === null) {
|
|
67
|
+
if (field.required)
|
|
68
|
+
issues.push({ at, message: `The required member "${field.wire}" is missing.` });
|
|
69
|
+
return defaultOf(field);
|
|
70
|
+
}
|
|
71
|
+
switch (field.kind) {
|
|
72
|
+
case "text": {
|
|
73
|
+
if (typeof value === "string" && value.length > 0)
|
|
74
|
+
return value;
|
|
75
|
+
issues.push({ at, message: `"${field.wire}" is not a non-empty string.` });
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
case "url": {
|
|
79
|
+
if (isUrl(value))
|
|
80
|
+
return new URL(value);
|
|
81
|
+
issues.push({ at, message: `"${field.wire}" is not an absolute URL.` });
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
case "boolean": {
|
|
85
|
+
if (typeof value === "boolean")
|
|
86
|
+
return value;
|
|
87
|
+
issues.push({ at, message: `"${field.wire}" is not a boolean.` });
|
|
88
|
+
return defaultOf(field);
|
|
89
|
+
}
|
|
90
|
+
case "urls":
|
|
91
|
+
case "strings": {
|
|
92
|
+
return readList(value, field, issues);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reads a list member, checking each entry, so a single bad entry is reported at its
|
|
99
|
+
* own index.
|
|
100
|
+
*
|
|
101
|
+
* @param value - The member's decoded value.
|
|
102
|
+
* @param field - The row being read.
|
|
103
|
+
* @param issues - Where problems are collected.
|
|
104
|
+
*/
|
|
105
|
+
function readList(value, field, issues) {
|
|
106
|
+
if (!Array.isArray(value)) {
|
|
107
|
+
issues.push({ at: pointer(field.wire), message: `"${field.wire}" is not an array.` });
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
let result = [];
|
|
111
|
+
for (let [index, entry] of value.entries()) {
|
|
112
|
+
let at = pointer(field.wire, index);
|
|
113
|
+
if (field.kind === "urls") {
|
|
114
|
+
if (isUrl(entry))
|
|
115
|
+
result.push(new URL(entry));
|
|
116
|
+
else
|
|
117
|
+
issues.push({ at, message: `"${field.wire}" lists a value that is not an absolute URL.` });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (typeof entry !== "string") {
|
|
121
|
+
issues.push({ at, message: `"${field.wire}" lists a value that is not a string.` });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (field.values && !field.values.includes(entry)) {
|
|
125
|
+
issues.push({
|
|
126
|
+
at,
|
|
127
|
+
message: `"${field.wire}" lists "${entry}", which is not one of ${field.values.join(", ")}.`,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
result.push(entry);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Reads a human-readable member and its `member#tag` variants into one value.
|
|
137
|
+
*
|
|
138
|
+
* @param body - The decoded document.
|
|
139
|
+
* @param field - The localized row.
|
|
140
|
+
* @param issues - Where problems are collected.
|
|
141
|
+
*/
|
|
142
|
+
function readLocalized(body, field, issues) {
|
|
143
|
+
let translations = {};
|
|
144
|
+
for (let [member, value] of Object.entries(body)) {
|
|
145
|
+
if (!member.startsWith(`${field.wire}#`))
|
|
146
|
+
continue;
|
|
147
|
+
if (typeof value === "string")
|
|
148
|
+
translations[member.slice(field.wire.length + 1)] = value;
|
|
149
|
+
else
|
|
150
|
+
issues.push({ at: pointer(member), message: `"${member}" is not a string.` });
|
|
151
|
+
}
|
|
152
|
+
let value = body[field.wire];
|
|
153
|
+
if (value === undefined || value === null) {
|
|
154
|
+
if (field.required)
|
|
155
|
+
issues.push({
|
|
156
|
+
at: pointer(field.wire),
|
|
157
|
+
message: `The required member "${field.wire}" is missing.`,
|
|
158
|
+
});
|
|
159
|
+
let [first] = Object.values(translations);
|
|
160
|
+
return first === undefined ? null : { value: first, translations };
|
|
161
|
+
}
|
|
162
|
+
if (typeof value !== "string") {
|
|
163
|
+
issues.push({ at: pointer(field.wire), message: `"${field.wire}" is not a string.` });
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return { value, translations };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Reads a metadata document through its table. Members outside the table become the
|
|
170
|
+
* extensions, validated by the schema when one is given. Every issue is collected
|
|
171
|
+
* before failing, so the error names all of them.
|
|
172
|
+
*
|
|
173
|
+
* @param text - The served JSON.
|
|
174
|
+
* @param options - The table, the extension schema and the identity check.
|
|
175
|
+
* @template Extensions - The extension members the schema produces.
|
|
176
|
+
*/
|
|
177
|
+
export function readMetadata(text, options) {
|
|
178
|
+
let decoded = readJsonObject(text, options.format);
|
|
179
|
+
if (decoded.status === "failure")
|
|
180
|
+
return decoded;
|
|
181
|
+
let body = decoded.data;
|
|
182
|
+
let issues = [];
|
|
183
|
+
let fields = {};
|
|
184
|
+
for (let [name, field] of Object.entries(options.table))
|
|
185
|
+
fields[name] = readField(body, field, issues);
|
|
186
|
+
if (issues.length === 0)
|
|
187
|
+
options.check?.(fields, issues);
|
|
188
|
+
let members = {};
|
|
189
|
+
for (let [member, value] of Object.entries(body)) {
|
|
190
|
+
if (!isStandardMember(options.table, member))
|
|
191
|
+
members[member] = value;
|
|
192
|
+
}
|
|
193
|
+
let extensions = readExtensions(members, options.extensions, issues);
|
|
194
|
+
if (issues.length > 0 || extensions === null) {
|
|
195
|
+
return failure(new WellKnownParseError(options.format, issues));
|
|
196
|
+
}
|
|
197
|
+
return success({ ...fields, extensions });
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Writes a document through its table, omitting `null` members, optional empty lists and
|
|
201
|
+
* default flags; a required list is written even when empty so the output parses back.
|
|
202
|
+
* Extensions never replace a standard member: any member name the table registers is dropped.
|
|
203
|
+
*
|
|
204
|
+
* @param document - The typed document.
|
|
205
|
+
* @param table - The document's mapping table.
|
|
206
|
+
*/
|
|
207
|
+
export function writeMetadata(document, table) {
|
|
208
|
+
let fields = document;
|
|
209
|
+
let output = {};
|
|
210
|
+
let extensions = fields.extensions;
|
|
211
|
+
if (typeof extensions === "object" && extensions !== null) {
|
|
212
|
+
for (let [member, value] of Object.entries(extensions)) {
|
|
213
|
+
if (!isStandardMember(table, member))
|
|
214
|
+
output[member] = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (let [name, field] of Object.entries(table)) {
|
|
218
|
+
let value = fields[name];
|
|
219
|
+
if (value === null || value === undefined)
|
|
220
|
+
continue;
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
if (value.length === 0 && !field.required)
|
|
223
|
+
continue;
|
|
224
|
+
output[field.wire] = value.map((entry) => (entry instanceof URL ? entry.href : entry));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (field.kind === "boolean") {
|
|
228
|
+
if (value !== (field.default ?? false))
|
|
229
|
+
output[field.wire] = value;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (field.kind === "localized") {
|
|
233
|
+
let localized = value;
|
|
234
|
+
output[field.wire] = localized.value;
|
|
235
|
+
for (let [tag, text] of Object.entries(localized.translations))
|
|
236
|
+
output[`${field.wire}#${tag}`] = text;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
output[field.wire] = value instanceof URL ? value.href : value;
|
|
240
|
+
}
|
|
241
|
+
return JSON.stringify(output);
|
|
242
|
+
}
|