@dwk/host-meta 0.1.0-beta.0
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 +15 -0
- package/README.md +90 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/document.d.ts +49 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +54 -0
- package/dist/document.js.map +1 -0
- package/dist/handler.d.ts +30 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +109 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/jrd.d.ts +33 -0
- package/dist/jrd.d.ts.map +1 -0
- package/dist/jrd.js +30 -0
- package/dist/jrd.js.map +1 -0
- package/dist/log.d.ts +30 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +28 -0
- package/dist/log.js.map +1 -0
- package/dist/negotiation.d.ts +21 -0
- package/dist/negotiation.d.ts.map +1 -0
- package/dist/negotiation.js +62 -0
- package/dist/negotiation.js.map +1 -0
- package/dist/xrd.d.ts +24 -0
- package/dist/xrd.d.ts.map +1 -0
- package/dist/xrd.js +100 -0
- package/dist/xrd.js.map +1 -0
- package/package.json +49 -0
- package/src/config.ts +96 -0
- package/src/document.ts +82 -0
- package/src/handler.ts +159 -0
- package/src/index.ts +48 -0
- package/src/jrd.ts +51 -0
- package/src/log.ts +32 -0
- package/src/negotiation.ts +65 -0
- package/src/xrd.ts +114 -0
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/host-meta` — structured observability event taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* host-meta is public, request-invariant discovery data, so the stakes are low
|
|
5
|
+
* — but which representation clients actually negotiate (XRD vs JRD) is useful
|
|
6
|
+
* signal, and a flood of rejected non-`GET` probes is worth a counter. Logging
|
|
7
|
+
* and metrics are opt-in via an injected {@link Logger}/{@link Metrics} (see
|
|
8
|
+
* `@dwk/log`) and **share this one vocabulary**: the same dotted event name is
|
|
9
|
+
* passed to the logger and the metrics sink so a log line and its counter line
|
|
10
|
+
* up. No request carries user data — the document is host-wide — so fields are
|
|
11
|
+
* limited to the chosen `format` and a machine-readable `reason`.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
/** Stable event names emitted by `@dwk/host-meta`. */
|
|
16
|
+
export declare const HostMetaLogEvent: {
|
|
17
|
+
/**
|
|
18
|
+
* The host-meta document was served. Field: `format` (`"xrd"` or `"jrd"`),
|
|
19
|
+
* the negotiated representation.
|
|
20
|
+
*/
|
|
21
|
+
readonly Served: "host-meta.served";
|
|
22
|
+
/**
|
|
23
|
+
* A request was rejected before the document could be served. Field: `reason`
|
|
24
|
+
* (`method_not_allowed`).
|
|
25
|
+
*/
|
|
26
|
+
readonly Rejected: "host-meta.rejected";
|
|
27
|
+
};
|
|
28
|
+
/** Union of the event-name string literals in {@link HostMetaLogEvent}. */
|
|
29
|
+
export type HostMetaLogEvent = (typeof HostMetaLogEvent)[keyof typeof HostMetaLogEvent];
|
|
30
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sDAAsD;AACtD,eAAO,MAAM,gBAAgB;IAC3B;;;OAGG;;IAEH;;;OAGG;;CAEK,CAAC;AAEX,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAC1B,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,OAAO,gBAAgB,CAAC,CAAC"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/host-meta` — structured observability event taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* host-meta is public, request-invariant discovery data, so the stakes are low
|
|
5
|
+
* — but which representation clients actually negotiate (XRD vs JRD) is useful
|
|
6
|
+
* signal, and a flood of rejected non-`GET` probes is worth a counter. Logging
|
|
7
|
+
* and metrics are opt-in via an injected {@link Logger}/{@link Metrics} (see
|
|
8
|
+
* `@dwk/log`) and **share this one vocabulary**: the same dotted event name is
|
|
9
|
+
* passed to the logger and the metrics sink so a log line and its counter line
|
|
10
|
+
* up. No request carries user data — the document is host-wide — so fields are
|
|
11
|
+
* limited to the chosen `format` and a machine-readable `reason`.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
/** Stable event names emitted by `@dwk/host-meta`. */
|
|
16
|
+
export const HostMetaLogEvent = {
|
|
17
|
+
/**
|
|
18
|
+
* The host-meta document was served. Field: `format` (`"xrd"` or `"jrd"`),
|
|
19
|
+
* the negotiated representation.
|
|
20
|
+
*/
|
|
21
|
+
Served: "host-meta.served",
|
|
22
|
+
/**
|
|
23
|
+
* A request was rejected before the document could be served. Field: `reason`
|
|
24
|
+
* (`method_not_allowed`).
|
|
25
|
+
*/
|
|
26
|
+
Rejected: "host-meta.rejected",
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sDAAsD;AACtD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B;;;OAGG;IACH,MAAM,EAAE,kBAAkB;IAC1B;;;OAGG;IACH,QAAQ,EAAE,oBAAoB;CACtB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Representation selection for the one `/.well-known/host-meta` URL (RFC 6415
|
|
3
|
+
* §3). The document is served as XRD (`application/xrd+xml`) **by default** and
|
|
4
|
+
* as JRD (`application/jrd+json`) only when the client specifically prefers it,
|
|
5
|
+
* via — in priority order — an explicit `?format=` override, the
|
|
6
|
+
* `host-meta.json` path (RFC 7033 §10.1, always JRD), or a higher-q Accept
|
|
7
|
+
* preference for JRD over XRD.
|
|
8
|
+
*/
|
|
9
|
+
/** The chosen representation. */
|
|
10
|
+
export type Format = "xrd" | "jrd";
|
|
11
|
+
/**
|
|
12
|
+
* Decide which representation to serve.
|
|
13
|
+
*
|
|
14
|
+
* 1. `?format=json`/`jrd` → JRD; `?format=xml`/`xrd` → XRD (explicit override
|
|
15
|
+
* wins over everything, per RFC 6415 §3's `?format=` mechanism).
|
|
16
|
+
* 2. A request to the `host-meta.json` path → JRD (RFC 7033 §10.1).
|
|
17
|
+
* 3. Otherwise compare Accept q-values: JRD only when it is *strictly*
|
|
18
|
+
* preferred over XRD; XRD is the default for ties, wildcards, and no header.
|
|
19
|
+
*/
|
|
20
|
+
export declare function negotiateFormat(url: URL, accept: string | null, isJsonPath: boolean): Format;
|
|
21
|
+
//# sourceMappingURL=negotiation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiation.d.ts","sourceRoot":"","sources":["../src/negotiation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,iCAAiC;AACjC,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AA2BnC;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,UAAU,EAAE,OAAO,GAClB,MAAM,CAcR"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Representation selection for the one `/.well-known/host-meta` URL (RFC 6415
|
|
3
|
+
* §3). The document is served as XRD (`application/xrd+xml`) **by default** and
|
|
4
|
+
* as JRD (`application/jrd+json`) only when the client specifically prefers it,
|
|
5
|
+
* via — in priority order — an explicit `?format=` override, the
|
|
6
|
+
* `host-meta.json` path (RFC 7033 §10.1, always JRD), or a higher-q Accept
|
|
7
|
+
* preference for JRD over XRD.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The best (highest) q-value among the given exact media types in an `Accept`
|
|
11
|
+
* header, or `0` if none of them appear. Wildcards (`* /*`) are deliberately
|
|
12
|
+
* ignored: they express no preference between XRD and JRD, so a bare `Accept:
|
|
13
|
+
* * /*` (or a missing header) must fall through to the XRD default.
|
|
14
|
+
*/
|
|
15
|
+
function acceptQuality(accept, mediaTypes) {
|
|
16
|
+
let best = 0;
|
|
17
|
+
for (const part of accept.split(",")) {
|
|
18
|
+
const segments = part.trim().split(";");
|
|
19
|
+
const type = segments[0]?.trim().toLowerCase();
|
|
20
|
+
if (type === undefined || !mediaTypes.includes(type))
|
|
21
|
+
continue;
|
|
22
|
+
let q = 1;
|
|
23
|
+
for (const segment of segments.slice(1)) {
|
|
24
|
+
const match = /^\s*q=(.*)$/i.exec(segment);
|
|
25
|
+
if (match && match[1] !== undefined) {
|
|
26
|
+
const parsed = Number.parseFloat(match[1]);
|
|
27
|
+
if (!Number.isNaN(parsed))
|
|
28
|
+
q = parsed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (q > best)
|
|
32
|
+
best = q;
|
|
33
|
+
}
|
|
34
|
+
return best;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Decide which representation to serve.
|
|
38
|
+
*
|
|
39
|
+
* 1. `?format=json`/`jrd` → JRD; `?format=xml`/`xrd` → XRD (explicit override
|
|
40
|
+
* wins over everything, per RFC 6415 §3's `?format=` mechanism).
|
|
41
|
+
* 2. A request to the `host-meta.json` path → JRD (RFC 7033 §10.1).
|
|
42
|
+
* 3. Otherwise compare Accept q-values: JRD only when it is *strictly*
|
|
43
|
+
* preferred over XRD; XRD is the default for ties, wildcards, and no header.
|
|
44
|
+
*/
|
|
45
|
+
export function negotiateFormat(url, accept, isJsonPath) {
|
|
46
|
+
const format = url.searchParams.get("format")?.toLowerCase();
|
|
47
|
+
if (format === "json" || format === "jrd")
|
|
48
|
+
return "jrd";
|
|
49
|
+
if (format === "xml" || format === "xrd")
|
|
50
|
+
return "xrd";
|
|
51
|
+
if (isJsonPath)
|
|
52
|
+
return "jrd";
|
|
53
|
+
if (accept === null || accept.length === 0)
|
|
54
|
+
return "xrd";
|
|
55
|
+
const jrdQuality = acceptQuality(accept, [
|
|
56
|
+
"application/jrd+json",
|
|
57
|
+
"application/json",
|
|
58
|
+
]);
|
|
59
|
+
const xrdQuality = acceptQuality(accept, ["application/xrd+xml"]);
|
|
60
|
+
return jrdQuality > xrdQuality ? "jrd" : "xrd";
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=negotiation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiation.js","sourceRoot":"","sources":["../src/negotiation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;;GAKG;AACH,SAAS,aAAa,CAAC,MAAc,EAAE,UAA6B;IAClE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC/C,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,SAAS;QAC/D,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3C,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;gBACpC,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC3C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;oBAAE,CAAC,GAAG,MAAM,CAAC;YACxC,CAAC;QACH,CAAC;QACD,IAAI,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAQ,EACR,MAAqB,EACrB,UAAmB;IAEnB,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7D,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACxD,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAEvD,IAAI,UAAU;QAAE,OAAO,KAAK,CAAC;IAE7B,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACzD,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,EAAE;QACvC,sBAAsB;QACtB,kBAAkB;KACnB,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAClE,OAAO,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AACjD,CAAC"}
|
package/dist/xrd.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XRD (Extensible Resource Descriptor) rendering of a host-meta document — the
|
|
3
|
+
* default `application/xrd+xml` representation of RFC 6415 §2. This is the only
|
|
4
|
+
* serializer new to `@dwk/host-meta`: the JRD side reuses `@dwk/webfinger`'s
|
|
5
|
+
* link shape. It renders the *same* {@link HostMetaDocument} the JRD serializer
|
|
6
|
+
* does, so the two stay information-equivalent (RFC 6415 §3).
|
|
7
|
+
*
|
|
8
|
+
* Output maps the model onto the XRD 1.0 schema: `Subject`, `Property`
|
|
9
|
+
* (`xsi:nil="true"` for an absent/`null` value), and `Link` elements with
|
|
10
|
+
* `rel`/`type`/`href`/`template` attributes and nested `Title`/`Property`
|
|
11
|
+
* children. All text and attribute values are XML-escaped.
|
|
12
|
+
*/
|
|
13
|
+
import type { HostMetaDocument } from "./document";
|
|
14
|
+
/** The XRD 1.0 default namespace (OASIS). */
|
|
15
|
+
export declare const XRD_NAMESPACE = "http://docs.oasis-open.org/ns/xri/xrd-1.0";
|
|
16
|
+
/**
|
|
17
|
+
* Escape a string for inclusion in XML text or a double-quoted attribute value:
|
|
18
|
+
* the five predefined entities. Applied to every dynamic value so operator- or
|
|
19
|
+
* resource-supplied content can never break out of the document structure.
|
|
20
|
+
*/
|
|
21
|
+
export declare function escapeXml(value: string): string;
|
|
22
|
+
/** Serialize a host-meta document to an XRD (`application/xrd+xml`) body. */
|
|
23
|
+
export declare function serializeXrd(document: HostMetaDocument): string;
|
|
24
|
+
//# sourceMappingURL=xrd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xrd.d.ts","sourceRoot":"","sources":["../src/xrd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAQ,MAAM,YAAY,CAAC;AAEzD,6CAA6C;AAC7C,eAAO,MAAM,aAAa,8CAA8C,CAAC;AAIzE;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO/C;AA0DD,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAsB/D"}
|
package/dist/xrd.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XRD (Extensible Resource Descriptor) rendering of a host-meta document — the
|
|
3
|
+
* default `application/xrd+xml` representation of RFC 6415 §2. This is the only
|
|
4
|
+
* serializer new to `@dwk/host-meta`: the JRD side reuses `@dwk/webfinger`'s
|
|
5
|
+
* link shape. It renders the *same* {@link HostMetaDocument} the JRD serializer
|
|
6
|
+
* does, so the two stay information-equivalent (RFC 6415 §3).
|
|
7
|
+
*
|
|
8
|
+
* Output maps the model onto the XRD 1.0 schema: `Subject`, `Property`
|
|
9
|
+
* (`xsi:nil="true"` for an absent/`null` value), and `Link` elements with
|
|
10
|
+
* `rel`/`type`/`href`/`template` attributes and nested `Title`/`Property`
|
|
11
|
+
* children. All text and attribute values are XML-escaped.
|
|
12
|
+
*/
|
|
13
|
+
/** The XRD 1.0 default namespace (OASIS). */
|
|
14
|
+
export const XRD_NAMESPACE = "http://docs.oasis-open.org/ns/xri/xrd-1.0";
|
|
15
|
+
/** The XML Schema instance namespace, used for `xsi:nil` on absent properties. */
|
|
16
|
+
const XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance";
|
|
17
|
+
/**
|
|
18
|
+
* Escape a string for inclusion in XML text or a double-quoted attribute value:
|
|
19
|
+
* the five predefined entities. Applied to every dynamic value so operator- or
|
|
20
|
+
* resource-supplied content can never break out of the document structure.
|
|
21
|
+
*/
|
|
22
|
+
export function escapeXml(value) {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/&/g, "&")
|
|
25
|
+
.replace(/</g, "<")
|
|
26
|
+
.replace(/>/g, ">")
|
|
27
|
+
.replace(/"/g, """)
|
|
28
|
+
.replace(/'/g, "'");
|
|
29
|
+
}
|
|
30
|
+
/** Whether any property (top-level or per-link) is `null`, requiring the `xsi` namespace. */
|
|
31
|
+
function needsXsi(document) {
|
|
32
|
+
// Mirror linkXml's null-safety: a missing properties bag (`undefined` or a
|
|
33
|
+
// runtime `null` from untyped callers) carries no nil values to declare.
|
|
34
|
+
const hasNil = (props) => props !== undefined &&
|
|
35
|
+
props !== null &&
|
|
36
|
+
Object.values(props).some((v) => v === null);
|
|
37
|
+
if (hasNil(document.properties))
|
|
38
|
+
return true;
|
|
39
|
+
return document.links.some((link) => hasNil(link.properties));
|
|
40
|
+
}
|
|
41
|
+
/** Render a single `<Property>` element at the given indent. */
|
|
42
|
+
function propertyXml(type, value, indent) {
|
|
43
|
+
if (value === null) {
|
|
44
|
+
return `${indent}<Property type="${escapeXml(type)}" xsi:nil="true"/>`;
|
|
45
|
+
}
|
|
46
|
+
return `${indent}<Property type="${escapeXml(type)}">${escapeXml(value)}</Property>`;
|
|
47
|
+
}
|
|
48
|
+
/** Render a single `<Link>` element, expanding to a child block only when it has titles/properties. */
|
|
49
|
+
function linkXml(link) {
|
|
50
|
+
const attrs = [`rel="${escapeXml(link.rel)}"`];
|
|
51
|
+
if (link.type !== undefined)
|
|
52
|
+
attrs.push(`type="${escapeXml(link.type)}"`);
|
|
53
|
+
if (link.href !== undefined)
|
|
54
|
+
attrs.push(`href="${escapeXml(link.href)}"`);
|
|
55
|
+
if (link.template !== undefined) {
|
|
56
|
+
attrs.push(`template="${escapeXml(link.template)}"`);
|
|
57
|
+
}
|
|
58
|
+
const open = ` <Link ${attrs.join(" ")}`;
|
|
59
|
+
const titles = link.titles ? Object.entries(link.titles) : [];
|
|
60
|
+
const properties = link.properties ? Object.entries(link.properties) : [];
|
|
61
|
+
if (titles.length === 0 && properties.length === 0) {
|
|
62
|
+
return [`${open}/>`];
|
|
63
|
+
}
|
|
64
|
+
const lines = [`${open}>`];
|
|
65
|
+
for (const [lang, title] of titles) {
|
|
66
|
+
// RFC 7033 §4.4.4.3 uses "und" for an undetermined language; XRD omits the
|
|
67
|
+
// attribute in that case rather than emitting `xml:lang="und"`.
|
|
68
|
+
const langAttr = lang === "und" ? "" : ` xml:lang="${escapeXml(lang)}"`;
|
|
69
|
+
lines.push(` <Title${langAttr}>${escapeXml(title)}</Title>`);
|
|
70
|
+
}
|
|
71
|
+
for (const [type, value] of properties) {
|
|
72
|
+
lines.push(propertyXml(type, value, " "));
|
|
73
|
+
}
|
|
74
|
+
lines.push(" </Link>");
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
/** Serialize a host-meta document to an XRD (`application/xrd+xml`) body. */
|
|
78
|
+
export function serializeXrd(document) {
|
|
79
|
+
const rootAttrs = needsXsi(document)
|
|
80
|
+
? `xmlns="${XRD_NAMESPACE}" xmlns:xsi="${XSI_NAMESPACE}"`
|
|
81
|
+
: `xmlns="${XRD_NAMESPACE}"`;
|
|
82
|
+
const lines = [
|
|
83
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
84
|
+
`<XRD ${rootAttrs}>`,
|
|
85
|
+
];
|
|
86
|
+
if (document.subject !== undefined) {
|
|
87
|
+
lines.push(` <Subject>${escapeXml(document.subject)}</Subject>`);
|
|
88
|
+
}
|
|
89
|
+
if (document.properties) {
|
|
90
|
+
for (const [type, value] of Object.entries(document.properties)) {
|
|
91
|
+
lines.push(propertyXml(type, value, " "));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const link of document.links) {
|
|
95
|
+
lines.push(...linkXml(link));
|
|
96
|
+
}
|
|
97
|
+
lines.push("</XRD>");
|
|
98
|
+
return lines.join("\n") + "\n";
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=xrd.js.map
|
package/dist/xrd.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xrd.js","sourceRoot":"","sources":["../src/xrd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,6CAA6C;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAG,2CAA2C,CAAC;AACzE,kFAAkF;AAClF,MAAM,aAAa,GAAG,2CAA2C,CAAC;AAElE;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,6FAA6F;AAC7F,SAAS,QAAQ,CAAC,QAA0B;IAC1C,2EAA2E;IAC3E,yEAAyE;IACzE,MAAM,MAAM,GAAG,CACb,KAAsD,EAC7C,EAAE,CACX,KAAK,KAAK,SAAS;QACnB,KAAK,KAAK,IAAI;QACd,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,gEAAgE;AAChE,SAAS,WAAW,CAClB,IAAY,EACZ,KAAoB,EACpB,MAAc;IAEd,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,GAAG,MAAM,mBAAmB,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC;IACzE,CAAC;IACD,OAAO,GAAG,MAAM,mBAAmB,SAAS,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC;AACvF,CAAC;AAED,uGAAuG;AACvG,SAAS,OAAO,CAAC,IAAU;IACzB,MAAM,KAAK,GAAG,CAAC,QAAQ,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1E,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1E,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,aAAa,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAE1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;IACvB,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QACnC,2EAA2E;QAC3E,gEAAgE;QAChE,MAAM,QAAQ,GAAG,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;QACxE,KAAK,CAAC,IAAI,CAAC,aAAa,QAAQ,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAClE,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,UAAU,EAAE,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACxB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAAC,QAA0B;IACrD,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAClC,CAAC,CAAC,UAAU,aAAa,gBAAgB,aAAa,GAAG;QACzD,CAAC,CAAC,UAAU,aAAa,GAAG,CAAC;IAE/B,MAAM,KAAK,GAAG;QACZ,wCAAwC;QACxC,QAAQ,SAAS,GAAG;KACrB,CAAC;IACF,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,cAAc,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAChE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/B,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dwk/host-meta",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Web Host Metadata (RFC 6415) host-meta discovery at /.well-known/host-meta with XRD/JRD content negotiation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"host-meta",
|
|
7
|
+
"rfc6415",
|
|
8
|
+
"well-known",
|
|
9
|
+
"discovery",
|
|
10
|
+
"xrd",
|
|
11
|
+
"jrd",
|
|
12
|
+
"cloudflare-workers"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"author": "David W. Keith <me@dwk.io>",
|
|
17
|
+
"homepage": "https://github.com/davidwkeith/workers/tree/main/packages/host-meta#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/davidwkeith/workers.git",
|
|
21
|
+
"directory": "packages/host-meta"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"!src/**/*.test.ts"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@dwk/log": "0.1.0-beta.0",
|
|
42
|
+
"@dwk/webfinger": "0.1.0-beta.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
47
|
+
"clean": "rm -rf dist"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for {@link createHostMeta}: the WebFinger endpoint URL that
|
|
3
|
+
* seeds the `lrdd` template and/or a set of static top-level links, plus the
|
|
4
|
+
* optional logging/metrics seams. Per the composition contract the package
|
|
5
|
+
* never reads the global environment — every link arrives here, so a handler
|
|
6
|
+
* can be instantiated multiple times and tested in isolation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { noopLogger, noopMetrics, type Logger, type Metrics } from "@dwk/log";
|
|
10
|
+
import type { Link } from "@dwk/webfinger";
|
|
11
|
+
|
|
12
|
+
import { buildDocument, type HostMetaDocument } from "./document";
|
|
13
|
+
import { serializeJrd } from "./jrd";
|
|
14
|
+
import { serializeXrd } from "./xrd";
|
|
15
|
+
|
|
16
|
+
/** Configuration passed to {@link createHostMeta}. */
|
|
17
|
+
export interface HostMetaConfig {
|
|
18
|
+
/**
|
|
19
|
+
* The site's WebFinger endpoint URL (e.g.
|
|
20
|
+
* `https://example.com/.well-known/webfinger`). When supplied, an `lrdd` link
|
|
21
|
+
* templated to it (`…?resource={uri}`) is emitted first — the relation
|
|
22
|
+
* fediverse/OpenID software fetches host-meta to find before falling back to
|
|
23
|
+
* WebFinger. A URL already containing `{uri}` is used verbatim.
|
|
24
|
+
*/
|
|
25
|
+
readonly webfingerUrl?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Additional static top-level `Link` entries to advertise (e.g. `author`,
|
|
28
|
+
* `license`). Appended after the auto-generated `lrdd` link. At least one of
|
|
29
|
+
* {@link webfingerUrl} or `links` MUST be supplied, or {@link createHostMeta}
|
|
30
|
+
* throws at construction time.
|
|
31
|
+
*/
|
|
32
|
+
readonly links?: readonly Link[];
|
|
33
|
+
/** Optional subject the metadata describes — the host itself (XRD `Subject`). */
|
|
34
|
+
readonly subject?: string;
|
|
35
|
+
/** Optional host-level properties; a `null` value means "present but absent". */
|
|
36
|
+
readonly properties?: Readonly<Record<string, string | null>>;
|
|
37
|
+
/**
|
|
38
|
+
* Logger for discovery events; defaults to a no-op. Wire a real logger (see
|
|
39
|
+
* `@dwk/log`) to surface which representation was served and why a request was
|
|
40
|
+
* rejected instead of swallowing them.
|
|
41
|
+
*/
|
|
42
|
+
readonly logger?: Logger;
|
|
43
|
+
/**
|
|
44
|
+
* Metrics sink for discovery counters; defaults to a no-op. Wire an adapter
|
|
45
|
+
* (e.g. `analyticsEngineMetrics` from `@dwk/log`) to chart the same events the
|
|
46
|
+
* logger names — "served/min" by format, "rejection rate".
|
|
47
|
+
*/
|
|
48
|
+
readonly metrics?: Metrics;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Configuration after defaults are filled and the document is pre-computed. */
|
|
52
|
+
export interface ResolvedConfig {
|
|
53
|
+
/** The host-meta document, built once at construction (it never varies per request). */
|
|
54
|
+
readonly document: HostMetaDocument;
|
|
55
|
+
/** The XRD representation, serialized once at construction. */
|
|
56
|
+
readonly xrdBody: string;
|
|
57
|
+
/** The JRD representation, serialized once at construction. */
|
|
58
|
+
readonly jrdBody: string;
|
|
59
|
+
readonly logger: Logger;
|
|
60
|
+
readonly metrics: Metrics;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate and normalise a {@link HostMetaConfig}. Fails loudly when neither a
|
|
65
|
+
* WebFinger URL nor any static link is configured — a host-meta document that
|
|
66
|
+
* advertises nothing is always a misconfiguration, not a silently empty `XRD`.
|
|
67
|
+
* The document is computed once here (it is request-invariant) and reused for
|
|
68
|
+
* every response.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveConfig(config: HostMetaConfig): ResolvedConfig {
|
|
71
|
+
const hasLinks = (config.links?.length ?? 0) > 0;
|
|
72
|
+
if (config.webfingerUrl === undefined && !hasLinks) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"@dwk/host-meta: configure `webfingerUrl` and/or `links` — a host-meta " +
|
|
75
|
+
"document must advertise at least one link.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The document is request-invariant, so both representations are serialized
|
|
80
|
+
// once here and reused verbatim for every response — no per-request XML
|
|
81
|
+
// serialization or JSON.stringify.
|
|
82
|
+
const document = buildDocument({
|
|
83
|
+
webfingerUrl: config.webfingerUrl,
|
|
84
|
+
links: config.links,
|
|
85
|
+
subject: config.subject,
|
|
86
|
+
properties: config.properties,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
document,
|
|
91
|
+
xrdBody: serializeXrd(document),
|
|
92
|
+
jrdBody: serializeJrd(document),
|
|
93
|
+
logger: config.logger ?? noopLogger,
|
|
94
|
+
metrics: config.metrics ?? noopMetrics,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/document.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host-meta document model (RFC 6415 §2): a host-wide set of top-level
|
|
3
|
+
* `Link`s plus optional `Subject`/`Property` metadata. The model is a single
|
|
4
|
+
* plain-data shape that **both** representations render from — the XRD
|
|
5
|
+
* (`application/xrd+xml`) serializer in `xrd.ts` and the JRD
|
|
6
|
+
* (`application/jrd+json`) serializer in `jrd.ts` — so the two are guaranteed
|
|
7
|
+
* information-equivalent (RFC 6415 §3).
|
|
8
|
+
*
|
|
9
|
+
* The `Link` type is reused verbatim from `@dwk/webfinger`: host-meta links are
|
|
10
|
+
* the same `rel`/`href`/`template`/`type`/`titles`/`properties` shape WebFinger
|
|
11
|
+
* already defines, so the JRD link-shaping is shared and only the XRD serializer
|
|
12
|
+
* is new (see the package spec).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Link } from "@dwk/webfinger";
|
|
16
|
+
|
|
17
|
+
export type { Link };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A resolved host-meta document. `links` is always present (it is the point of
|
|
21
|
+
* the document); `subject` and `properties` appear only when the operator
|
|
22
|
+
* configured them. This is what both serializers consume.
|
|
23
|
+
*/
|
|
24
|
+
export interface HostMetaDocument {
|
|
25
|
+
/** Optional subject the metadata describes — the host itself (XRD `Subject`). */
|
|
26
|
+
readonly subject?: string;
|
|
27
|
+
/** Optional host-level properties; a `null` value means "present but absent". */
|
|
28
|
+
readonly properties?: Readonly<Record<string, string | null>>;
|
|
29
|
+
/** The top-level link relations advertised for the host. */
|
|
30
|
+
readonly links: readonly Link[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the `lrdd` `template` from a WebFinger endpoint URL. The Link-based
|
|
35
|
+
* Resource Descriptor Document template is the URL clients expand with the
|
|
36
|
+
* queried resource (`{uri}`) to reach per-account discovery (RFC 6415 §3,
|
|
37
|
+
* RFC 7033 §10.1). If the supplied URL already contains a `{uri}` placeholder it
|
|
38
|
+
* is used as-is; otherwise a `resource={uri}` query parameter is appended.
|
|
39
|
+
*/
|
|
40
|
+
export function lrddTemplate(webfingerUrl: string): string {
|
|
41
|
+
if (webfingerUrl.includes("{uri}")) return webfingerUrl;
|
|
42
|
+
const separator = webfingerUrl.includes("?") ? "&" : "?";
|
|
43
|
+
return `${webfingerUrl}${separator}resource={uri}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Assemble the host-meta {@link HostMetaDocument} from the operator's inputs.
|
|
48
|
+
* When a WebFinger endpoint URL is supplied, an `lrdd` link templated to it is
|
|
49
|
+
* emitted **first** (the relation fediverse/OpenID software probes for before
|
|
50
|
+
* falling back to WebFinger), followed by any static operator-configured links.
|
|
51
|
+
*/
|
|
52
|
+
export function buildDocument(opts: {
|
|
53
|
+
readonly webfingerUrl?: string;
|
|
54
|
+
readonly links?: readonly Link[];
|
|
55
|
+
readonly subject?: string;
|
|
56
|
+
readonly properties?: Readonly<Record<string, string | null>>;
|
|
57
|
+
}): HostMetaDocument {
|
|
58
|
+
const links: Link[] = [];
|
|
59
|
+
if (opts.webfingerUrl !== undefined) {
|
|
60
|
+
links.push({
|
|
61
|
+
rel: "lrdd",
|
|
62
|
+
type: "application/jrd+json",
|
|
63
|
+
template: lrddTemplate(opts.webfingerUrl),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (opts.links) {
|
|
67
|
+
links.push(...opts.links);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const document: {
|
|
71
|
+
subject?: string;
|
|
72
|
+
properties?: Readonly<Record<string, string | null>>;
|
|
73
|
+
links: readonly Link[];
|
|
74
|
+
} = { links };
|
|
75
|
+
if (opts.subject !== undefined) {
|
|
76
|
+
document.subject = opts.subject;
|
|
77
|
+
}
|
|
78
|
+
if (opts.properties && Object.keys(opts.properties).length > 0) {
|
|
79
|
+
document.properties = opts.properties;
|
|
80
|
+
}
|
|
81
|
+
return document;
|
|
82
|
+
}
|