@dwk/webfinger 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 +106 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -0
- package/dist/handler.d.ts +29 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +130 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/jrd.d.ts +73 -0
- package/dist/jrd.d.ts.map +1 -0
- package/dist/jrd.js +38 -0
- package/dist/jrd.js.map +1 -0
- package/dist/log.d.ts +36 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +34 -0
- package/dist/log.js.map +1 -0
- package/dist/resource.d.ts +35 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +79 -0
- package/dist/resource.js.map +1 -0
- package/package.json +46 -0
- package/src/config.ts +120 -0
- package/src/handler.ts +183 -0
- package/src/index.ts +44 -0
- package/src/jrd.ts +104 -0
- package/src/log.ts +38 -0
- package/src/resource.ts +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David W. Keith
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# `@dwk/webfinger`
|
|
2
|
+
|
|
3
|
+
> WebFinger (RFC 7033) account/resource discovery endpoint. Endpoint package.
|
|
4
|
+
|
|
5
|
+
Part of the [`@dwk` IndieWeb + Solid cohort](../../README.md). See the
|
|
6
|
+
[package specification](../../spec/packages/webfinger.md) for the full
|
|
7
|
+
requirements.
|
|
8
|
+
|
|
9
|
+
WebFinger maps a queried `resource` URI (`acct:`, `mailto:`, `https:`) to a
|
|
10
|
+
**JSON Resource Descriptor** (JRD) of links — profile page, avatar, OIDC issuer,
|
|
11
|
+
the `self` ActivityPub actor. It is the foundational discovery step for
|
|
12
|
+
federation: fediverse software resolves `acct:user@domain` against
|
|
13
|
+
`/.well-known/webfinger` before it can follow or address an account.
|
|
14
|
+
|
|
15
|
+
## Worker vs. static (why this package exists)
|
|
16
|
+
|
|
17
|
+
WebFinger is **borderline static**. A static site generator can emit a single
|
|
18
|
+
`/.well-known/webfinger` JRD, and for a **single-identity** site that often
|
|
19
|
+
suffices. Spec-correct behaviour, however, needs request logic a static host
|
|
20
|
+
cannot do:
|
|
21
|
+
|
|
22
|
+
- dispatch on the `resource` query parameter and return **404** for a resource
|
|
23
|
+
this server does not control (a static file returns `200` for any `resource=`);
|
|
24
|
+
- echo the matched `subject`, which **must equal** the queried `resource` URI
|
|
25
|
+
(fediverse software rejects a mismatch);
|
|
26
|
+
- filter the returned `links` by the `rel` query parameter(s).
|
|
27
|
+
|
|
28
|
+
So: ship the package for correct multi-resource / `rel`-filtered behaviour; the
|
|
29
|
+
degenerate single-resource, no-`rel` case may stay a static file.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createWebfinger } from "@dwk/webfinger";
|
|
35
|
+
|
|
36
|
+
const webfinger = createWebfinger({
|
|
37
|
+
resources: {
|
|
38
|
+
"acct:alice@example.com": {
|
|
39
|
+
aliases: ["https://example.com/users/alice"],
|
|
40
|
+
links: [
|
|
41
|
+
{
|
|
42
|
+
rel: "http://webfinger.net/rel/profile-page",
|
|
43
|
+
type: "text/html",
|
|
44
|
+
href: "https://example.com/",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
rel: "self",
|
|
48
|
+
type: "application/activity+json",
|
|
49
|
+
href: "https://example.com/users/alice",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// In your Worker's fetch handler, mount at the well-known path:
|
|
57
|
+
// GET /.well-known/webfinger?resource=acct:alice@example.com
|
|
58
|
+
return webfinger(request, env, ctx);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `resource` absent → **400**; `resource` not controlled → **404**; matched →
|
|
62
|
+
**200** with `subject` (echoing the queried URI), `aliases`, and `links`.
|
|
63
|
+
- Matching is **case-insensitive on the scheme and host** per RFC 7033 §4.1
|
|
64
|
+
(`ACCT:alice@EXAMPLE.COM` matches a configured `acct:alice@example.com`); the
|
|
65
|
+
`acct:` local part stays case-sensitive. The echoed `subject` keeps the
|
|
66
|
+
client's literal spelling.
|
|
67
|
+
- A `rel` parameter (repeatable) filters `links` to the matching relations;
|
|
68
|
+
`aliases` and `properties` are unaffected.
|
|
69
|
+
- Every response — success or error — carries `Access-Control-Allow-Origin: *`
|
|
70
|
+
per RFC 7033 §10.2, because discovery data is public. `OPTIONS` returns a CORS
|
|
71
|
+
preflight; non-`GET`/`HEAD` methods get **405**.
|
|
72
|
+
- The response media type is `application/jrd+json`.
|
|
73
|
+
|
|
74
|
+
### Dynamic resolution
|
|
75
|
+
|
|
76
|
+
For a resource set that is large or derived from stored data, pass a `resolve`
|
|
77
|
+
function instead of (or alongside) the static `resources` map. The static map is
|
|
78
|
+
consulted first; the resolver is the fallback. Returning `undefined` yields a
|
|
79
|
+
`404`.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const webfinger = createWebfinger({
|
|
83
|
+
resolve: async (resource, rels) => {
|
|
84
|
+
const profile = await lookupProfile(resource); // your data source
|
|
85
|
+
return profile ? { links: profile.toWebfingerLinks(rels) } : undefined;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`createWebfinger` **fails loudly** at construction if neither `resources` nor
|
|
91
|
+
`resolve` is supplied — a WebFinger endpoint that controls no resources is always
|
|
92
|
+
a misconfiguration.
|
|
93
|
+
|
|
94
|
+
## Design
|
|
95
|
+
|
|
96
|
+
Pure and **stateless**: no Durable Object, no D1, and no required Cloudflare
|
|
97
|
+
bindings — the `resource → JRD` mapping is config-supplied (composition
|
|
98
|
+
contract), never read from the global environment. The discovery logic
|
|
99
|
+
unit-tests under Node without a Workers runtime.
|
|
100
|
+
|
|
101
|
+
## Observability
|
|
102
|
+
|
|
103
|
+
Discovery events are emitted through the injected `@dwk/log` `Logger`/`Metrics`
|
|
104
|
+
seams (default no-op): `webfinger.resolved` for a match, `webfinger.rejected`
|
|
105
|
+
for a `400`/`404`/`405`. A queried `resource` is reduced to its **host** in logs
|
|
106
|
+
— the local part of an `acct:` handle is never recorded.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for {@link createWebfinger}: the set of resources this server
|
|
3
|
+
* controls (a static map, a resolver function, or both) plus the optional
|
|
4
|
+
* logging/metrics seams. Per the composition contract the package never reads
|
|
5
|
+
* the global environment — every resource mapping arrives here, so a handler can
|
|
6
|
+
* be instantiated multiple times and tested in isolation.
|
|
7
|
+
*/
|
|
8
|
+
import { type Logger, type Metrics } from "@dwk/log";
|
|
9
|
+
import type { ResourceRecord } from "./jrd";
|
|
10
|
+
/**
|
|
11
|
+
* A dynamic resolver from a queried `resource` URI to its {@link ResourceRecord},
|
|
12
|
+
* or `undefined` when this server does not control the resource (→ `404`). The
|
|
13
|
+
* `resource` is passed **normalized** (lowercased scheme/host per RFC 7033 §4.1),
|
|
14
|
+
* so a resolver can compare it directly without re-normalizing. The matched
|
|
15
|
+
* `rel` filters are passed through so a resolver backed by stored data (e.g. a
|
|
16
|
+
* profile document via `@dwk/rdf`) can avoid materialising links it is about to
|
|
17
|
+
* discard, but applying the filter is optional — the handler always re-applies
|
|
18
|
+
* {@link filterLinksByRel} to whatever is returned.
|
|
19
|
+
*/
|
|
20
|
+
export type ResourceResolver = (resource: string, rels: readonly string[]) => ResourceRecord | undefined | Promise<ResourceRecord | undefined>;
|
|
21
|
+
/** Configuration passed to {@link createWebfinger}. */
|
|
22
|
+
export interface WebfingerConfig {
|
|
23
|
+
/**
|
|
24
|
+
* Static map of controlled `resource` URI → its descriptor. Keys are the
|
|
25
|
+
* exact `resource` values clients query (`acct:user@example.com`,
|
|
26
|
+
* `https://example.com/`, `mailto:user@example.com`). Consulted before
|
|
27
|
+
* {@link resolve}.
|
|
28
|
+
*/
|
|
29
|
+
readonly resources?: Readonly<Record<string, ResourceRecord>>;
|
|
30
|
+
/**
|
|
31
|
+
* Dynamic fallback consulted when {@link resources} has no entry for the
|
|
32
|
+
* queried URI. At least one of {@link resources} or `resolve` MUST be
|
|
33
|
+
* supplied, or {@link createWebfinger} throws at construction time.
|
|
34
|
+
*/
|
|
35
|
+
readonly resolve?: ResourceResolver;
|
|
36
|
+
/**
|
|
37
|
+
* Logger for discovery events; defaults to a no-op. Wire a real logger (see
|
|
38
|
+
* `@dwk/log`) to surface unknown-resource probes and method rejections instead
|
|
39
|
+
* of swallowing them.
|
|
40
|
+
*/
|
|
41
|
+
readonly logger?: Logger;
|
|
42
|
+
/**
|
|
43
|
+
* Metrics sink for discovery counters; defaults to a no-op. Wire an adapter
|
|
44
|
+
* (e.g. `analyticsEngineMetrics` from `@dwk/log`) to chart the same events the
|
|
45
|
+
* logger names — "lookups/min", "404 rate", "rel-filtered lookups".
|
|
46
|
+
*/
|
|
47
|
+
readonly metrics?: Metrics;
|
|
48
|
+
}
|
|
49
|
+
/** Configuration after defaults are filled and a unified resolver is built. */
|
|
50
|
+
export interface ResolvedConfig {
|
|
51
|
+
/** Look up a resource: static map first, then the dynamic resolver. */
|
|
52
|
+
readonly resolve: (resource: string, rels: readonly string[]) => Promise<ResourceRecord | undefined>;
|
|
53
|
+
readonly logger: Logger;
|
|
54
|
+
readonly metrics: Metrics;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate and normalise a {@link WebfingerConfig}. Fails loudly when neither a
|
|
58
|
+
* resource map nor a resolver is configured — a WebFinger endpoint that controls
|
|
59
|
+
* no resources is always a misconfiguration, not a silent "everything is a 404".
|
|
60
|
+
* Returns a single `resolve` that consults the static map first (an O(1) exact
|
|
61
|
+
* match) and then the dynamic resolver.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveConfig(config: WebfingerConfig): ResolvedConfig;
|
|
64
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA2B,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAE9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,cAAc,GAAG,SAAS,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;AAEtE,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAC9D;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,CAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,CA4CrE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for {@link createWebfinger}: the set of resources this server
|
|
3
|
+
* controls (a static map, a resolver function, or both) plus the optional
|
|
4
|
+
* logging/metrics seams. Per the composition contract the package never reads
|
|
5
|
+
* the global environment — every resource mapping arrives here, so a handler can
|
|
6
|
+
* be instantiated multiple times and tested in isolation.
|
|
7
|
+
*/
|
|
8
|
+
import { noopLogger, noopMetrics } from "@dwk/log";
|
|
9
|
+
import { normalizeResource } from "./resource";
|
|
10
|
+
/**
|
|
11
|
+
* Validate and normalise a {@link WebfingerConfig}. Fails loudly when neither a
|
|
12
|
+
* resource map nor a resolver is configured — a WebFinger endpoint that controls
|
|
13
|
+
* no resources is always a misconfiguration, not a silent "everything is a 404".
|
|
14
|
+
* Returns a single `resolve` that consults the static map first (an O(1) exact
|
|
15
|
+
* match) and then the dynamic resolver.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveConfig(config) {
|
|
18
|
+
if (config.resources === undefined && config.resolve === undefined) {
|
|
19
|
+
throw new Error("@dwk/webfinger: configure `resources` and/or `resolve` — a WebFinger " +
|
|
20
|
+
"endpoint must control at least one resource source.");
|
|
21
|
+
}
|
|
22
|
+
// Key the static map by the normalized resource URI so matching is
|
|
23
|
+
// case-insensitive on scheme/host (RFC 7033 §4.1). When two configured keys
|
|
24
|
+
// normalize to the same value, the later entry wins.
|
|
25
|
+
const normalizedMap = config.resources === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: new Map(Object.entries(config.resources).map(([key, record]) => [
|
|
28
|
+
normalizeResource(key),
|
|
29
|
+
record,
|
|
30
|
+
]));
|
|
31
|
+
const dynamicResolve = config.resolve;
|
|
32
|
+
const resolve = async (resource, rels) => {
|
|
33
|
+
const key = normalizeResource(resource);
|
|
34
|
+
if (normalizedMap !== undefined) {
|
|
35
|
+
const record = normalizedMap.get(key);
|
|
36
|
+
if (record !== undefined) {
|
|
37
|
+
return record;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (dynamicResolve !== undefined) {
|
|
41
|
+
return await dynamicResolve(key, rels);
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
resolve,
|
|
47
|
+
logger: config.logger ?? noopLogger,
|
|
48
|
+
metrics: config.metrics ?? noopMetrics,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAA6B,MAAM,UAAU,CAAC;AAG9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAyD/C;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAuB;IACnD,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,qDAAqD,CACxD,CAAC;IACJ,CAAC;IAED,mEAAmE;IACnE,4EAA4E;IAC5E,qDAAqD;IACrD,MAAM,aAAa,GACjB,MAAM,CAAC,SAAS,KAAK,SAAS;QAC5B,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,IAAI,GAAG,CACL,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;YACtD,iBAAiB,CAAC,GAAG,CAAC;YACtB,MAAM;SACP,CAAC,CACH,CAAC;IACR,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC;IAEtC,MAAM,OAAO,GAAG,KAAK,EACnB,QAAgB,EAChB,IAAuB,EACc,EAAE;QACvC,MAAM,GAAG,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACtC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;QACD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;YACjC,OAAO,MAAM,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,OAAO;QACL,OAAO;QACP,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,UAAU;QACnC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,WAAW;KACvC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebFinger fetch handler (RFC 7033): a stateless `GET` endpoint, mountable
|
|
3
|
+
* at `/.well-known/webfinger`, that dispatches on the `resource` query parameter,
|
|
4
|
+
* returns the matching JRD (`rel`-filtered), and otherwise distinguishes a
|
|
5
|
+
* missing or malformed parameter (`400`) from an uncontrolled resource (`404`).
|
|
6
|
+
* Discovery data is public, so every response carries permissive CORS (§10.2).
|
|
7
|
+
*/
|
|
8
|
+
import { type WebfingerConfig } from "./config";
|
|
9
|
+
/**
|
|
10
|
+
* Cloudflare bindings required by the WebFinger handler: **none**. The resource
|
|
11
|
+
* mapping is config-supplied (composition contract), so this fragment is empty
|
|
12
|
+
* and contributes nothing to the composed Worker's `Env`.
|
|
13
|
+
*/
|
|
14
|
+
export type WebfingerEnv = Record<never, never>;
|
|
15
|
+
/** A `fetch`-compatible Worker handler. */
|
|
16
|
+
export type WebfingerHandler = (request: Request, env: WebfingerEnv, ctx: ExecutionContext) => Promise<Response>;
|
|
17
|
+
/**
|
|
18
|
+
* Build the WebFinger handler from configuration.
|
|
19
|
+
*
|
|
20
|
+
* The returned handler is mountable at `/.well-known/webfinger`. It accepts
|
|
21
|
+
* `GET` (and `HEAD`): a request with no `resource` parameter — or a malformed
|
|
22
|
+
* one (no scheme / unparseable URI, RFC 7033 §4.2) — gets `400`; a `resource`
|
|
23
|
+
* this server does not control gets `404`; a match gets `200` with an
|
|
24
|
+
* `application/jrd+json` body whose `subject` echoes the queried URI, filtered by
|
|
25
|
+
* any `rel` parameters. `OPTIONS` returns a CORS preflight; other methods get
|
|
26
|
+
* `405`. Fails loudly at construction if no resource source is configured.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createWebfinger(config: WebfingerConfig): WebfingerHandler;
|
|
29
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,UAAU,CAAC;AAKlB;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAEhD,2CAA2C;AAC3C,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAqEvB;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,CAuEzE"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebFinger fetch handler (RFC 7033): a stateless `GET` endpoint, mountable
|
|
3
|
+
* at `/.well-known/webfinger`, that dispatches on the `resource` query parameter,
|
|
4
|
+
* returns the matching JRD (`rel`-filtered), and otherwise distinguishes a
|
|
5
|
+
* missing or malformed parameter (`400`) from an uncontrolled resource (`404`).
|
|
6
|
+
* Discovery data is public, so every response carries permissive CORS (§10.2).
|
|
7
|
+
*/
|
|
8
|
+
import { hostFromUrl } from "@dwk/log";
|
|
9
|
+
import { resolveConfig, } from "./config";
|
|
10
|
+
import { buildJrd } from "./jrd";
|
|
11
|
+
import { WebfingerLogEvent } from "./log";
|
|
12
|
+
import { isWellFormedResource } from "./resource";
|
|
13
|
+
/** Media type for a JSON Resource Descriptor (RFC 7033 §10.2). */
|
|
14
|
+
const JRD_CONTENT_TYPE = "application/jrd+json; charset=utf-8";
|
|
15
|
+
const TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
|
|
16
|
+
/**
|
|
17
|
+
* WebFinger serves public discovery data to any origin, so every response —
|
|
18
|
+
* success or error — advertises permissive CORS per RFC 7033 §10.2.
|
|
19
|
+
*/
|
|
20
|
+
const CORS_HEADERS = {
|
|
21
|
+
"access-control-allow-origin": "*",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Reduce a queried `resource` to its host for logging: the domain of an
|
|
25
|
+
* `acct:`/`mailto:` handle, or the host of an `https:`/`http:` URI. The local
|
|
26
|
+
* part (the user identifier) is deliberately dropped — only the domain is
|
|
27
|
+
* recorded (see `log.ts`).
|
|
28
|
+
*/
|
|
29
|
+
function resourceHost(resource) {
|
|
30
|
+
if (/^(acct|mailto):/i.test(resource)) {
|
|
31
|
+
const at = resource.lastIndexOf("@");
|
|
32
|
+
if (at !== -1) {
|
|
33
|
+
const host = resource.slice(at + 1).toLowerCase();
|
|
34
|
+
return host.length > 0 ? host : undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return hostFromUrl(resource);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Emit a structured event on both the logger and the metrics seam, which share
|
|
42
|
+
* one event vocabulary (see `@dwk/log`): `warn` for rejections, `info` for a
|
|
43
|
+
* resolved lookup. Callers pass only sanitized hosts, reason codes, and counts.
|
|
44
|
+
*/
|
|
45
|
+
function emit(config, level, event, fields) {
|
|
46
|
+
config.logger[level](event, fields);
|
|
47
|
+
config.metrics.count(event, fields);
|
|
48
|
+
}
|
|
49
|
+
function jrdResponse(body, method) {
|
|
50
|
+
return new Response(method === "HEAD" ? null : body, {
|
|
51
|
+
status: 200,
|
|
52
|
+
headers: { "content-type": JRD_CONTENT_TYPE, ...CORS_HEADERS },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function errorResponse(status, message, extraHeaders) {
|
|
56
|
+
return new Response(message, {
|
|
57
|
+
status,
|
|
58
|
+
headers: {
|
|
59
|
+
"content-type": TEXT_CONTENT_TYPE,
|
|
60
|
+
...CORS_HEADERS,
|
|
61
|
+
...extraHeaders,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the WebFinger handler from configuration.
|
|
67
|
+
*
|
|
68
|
+
* The returned handler is mountable at `/.well-known/webfinger`. It accepts
|
|
69
|
+
* `GET` (and `HEAD`): a request with no `resource` parameter — or a malformed
|
|
70
|
+
* one (no scheme / unparseable URI, RFC 7033 §4.2) — gets `400`; a `resource`
|
|
71
|
+
* this server does not control gets `404`; a match gets `200` with an
|
|
72
|
+
* `application/jrd+json` body whose `subject` echoes the queried URI, filtered by
|
|
73
|
+
* any `rel` parameters. `OPTIONS` returns a CORS preflight; other methods get
|
|
74
|
+
* `405`. Fails loudly at construction if no resource source is configured.
|
|
75
|
+
*/
|
|
76
|
+
export function createWebfinger(config) {
|
|
77
|
+
const resolved = resolveConfig(config);
|
|
78
|
+
return async (request, _env, _ctx) => {
|
|
79
|
+
const method = request.method;
|
|
80
|
+
if (method === "OPTIONS") {
|
|
81
|
+
return new Response(null, {
|
|
82
|
+
status: 204,
|
|
83
|
+
headers: {
|
|
84
|
+
...CORS_HEADERS,
|
|
85
|
+
"access-control-allow-methods": "GET, HEAD, OPTIONS",
|
|
86
|
+
"access-control-allow-headers": "*",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
91
|
+
emit(resolved, "warn", WebfingerLogEvent.Rejected, {
|
|
92
|
+
reason: "method_not_allowed",
|
|
93
|
+
});
|
|
94
|
+
return errorResponse(405, "method_not_allowed: use GET", {
|
|
95
|
+
allow: "GET, HEAD, OPTIONS",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const url = new URL(request.url);
|
|
99
|
+
const resource = url.searchParams.get("resource");
|
|
100
|
+
if (resource === null || resource.length === 0) {
|
|
101
|
+
emit(resolved, "warn", WebfingerLogEvent.Rejected, {
|
|
102
|
+
reason: "missing_resource",
|
|
103
|
+
});
|
|
104
|
+
return errorResponse(400, "missing_resource: the `resource` query parameter is required");
|
|
105
|
+
}
|
|
106
|
+
if (!isWellFormedResource(resource)) {
|
|
107
|
+
emit(resolved, "warn", WebfingerLogEvent.Rejected, {
|
|
108
|
+
reason: "malformed_resource",
|
|
109
|
+
resourceHost: resourceHost(resource),
|
|
110
|
+
});
|
|
111
|
+
return errorResponse(400, "malformed_resource: the `resource` query parameter is not a valid URI");
|
|
112
|
+
}
|
|
113
|
+
const rels = url.searchParams.getAll("rel").filter((rel) => rel.length > 0);
|
|
114
|
+
const record = await resolved.resolve(resource, rels);
|
|
115
|
+
if (record === undefined) {
|
|
116
|
+
emit(resolved, "warn", WebfingerLogEvent.Rejected, {
|
|
117
|
+
reason: "not_found",
|
|
118
|
+
resourceHost: resourceHost(resource),
|
|
119
|
+
});
|
|
120
|
+
return errorResponse(404, "not_found: no descriptor for the requested resource");
|
|
121
|
+
}
|
|
122
|
+
const jrd = buildJrd(resource, record, rels);
|
|
123
|
+
emit(resolved, "info", WebfingerLogEvent.Resolved, {
|
|
124
|
+
resourceHost: resourceHost(resource),
|
|
125
|
+
relCount: rels.length,
|
|
126
|
+
});
|
|
127
|
+
return jrdResponse(JSON.stringify(jrd), method);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/webfinger` — WebFinger (RFC 7033) account/resource discovery endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint package: exports a factory returning a `fetch`-compatible handler,
|
|
5
|
+
* mountable at `/.well-known/webfinger` so it composes with other `@dwk`
|
|
6
|
+
* packages in one Worker. It maps a queried `resource` URI (`acct:`, `mailto:`,
|
|
7
|
+
* `https:`) to a JSON Resource Descriptor (JRD) of links — avatar, profile page,
|
|
8
|
+
* OIDC issuer, the `self` ActivityPub actor — and is the foundational discovery
|
|
9
|
+
* step for federation.
|
|
10
|
+
*
|
|
11
|
+
* It exists because WebFinger is **borderline static**: a static host can emit a
|
|
12
|
+
* single JRD, but only request logic can dispatch on `resource` (404 for a URI
|
|
13
|
+
* this server does not control), echo the matched `subject`, and filter `links`
|
|
14
|
+
* by `rel`. The package is pure and stateless — the `resource → JRD` mapping is
|
|
15
|
+
* supplied by config (a static map and/or a resolver), never read from the
|
|
16
|
+
* global environment — so it ships no Durable Object and needs no bindings, and
|
|
17
|
+
* the discovery logic unit-tests under Node without a Workers runtime.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/packages/webfinger.md
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
export { createWebfinger } from "./handler";
|
|
23
|
+
export type { WebfingerEnv, WebfingerHandler } from "./handler";
|
|
24
|
+
export { resolveConfig, type WebfingerConfig, type ResourceResolver, type ResolvedConfig, } from "./config";
|
|
25
|
+
export { filterLinksByRel, buildJrd, type Jrd, type Link, type ResourceRecord, } from "./jrd";
|
|
26
|
+
export { normalizeResource, isWellFormedResource } from "./resource";
|
|
27
|
+
export { WebfingerLogEvent } from "./log";
|
|
28
|
+
export type { Logger, Metrics } from "@dwk/log";
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAEhE,OAAO,EACL,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,gBAAgB,EAChB,QAAQ,EACR,KAAK,GAAG,EACR,KAAK,IAAI,EACT,KAAK,cAAc,GACpB,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAErE,OAAO,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAC1C,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/webfinger` — WebFinger (RFC 7033) account/resource discovery endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint package: exports a factory returning a `fetch`-compatible handler,
|
|
5
|
+
* mountable at `/.well-known/webfinger` so it composes with other `@dwk`
|
|
6
|
+
* packages in one Worker. It maps a queried `resource` URI (`acct:`, `mailto:`,
|
|
7
|
+
* `https:`) to a JSON Resource Descriptor (JRD) of links — avatar, profile page,
|
|
8
|
+
* OIDC issuer, the `self` ActivityPub actor — and is the foundational discovery
|
|
9
|
+
* step for federation.
|
|
10
|
+
*
|
|
11
|
+
* It exists because WebFinger is **borderline static**: a static host can emit a
|
|
12
|
+
* single JRD, but only request logic can dispatch on `resource` (404 for a URI
|
|
13
|
+
* this server does not control), echo the matched `subject`, and filter `links`
|
|
14
|
+
* by `rel`. The package is pure and stateless — the `resource → JRD` mapping is
|
|
15
|
+
* supplied by config (a static map and/or a resolver), never read from the
|
|
16
|
+
* global environment — so it ships no Durable Object and needs no bindings, and
|
|
17
|
+
* the discovery logic unit-tests under Node without a Workers runtime.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/packages/webfinger.md
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
export { createWebfinger } from "./handler";
|
|
23
|
+
export { resolveConfig, } from "./config";
|
|
24
|
+
export { filterLinksByRel, buildJrd, } from "./jrd";
|
|
25
|
+
export { normalizeResource, isWellFormedResource } from "./resource";
|
|
26
|
+
export { WebfingerLogEvent } from "./log";
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAG5C,OAAO,EACL,aAAa,GAId,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,gBAAgB,EAChB,QAAQ,GAIT,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAErE,OAAO,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC"}
|
package/dist/jrd.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Resource Descriptor (JRD) data model and the pure `rel`-filtering rule
|
|
3
|
+
* from RFC 7033 §4.3. These types are plain data — no Workers runtime, no
|
|
4
|
+
* Cloudflare bindings — so the discovery logic unit-tests in isolation. The
|
|
5
|
+
* handler in `handler.ts` turns an HTTP request into a `resource` lookup and
|
|
6
|
+
* renders the resulting {@link Jrd} as `application/jrd+json`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A single link relation in a JRD (RFC 7033 §4.4.4). `rel` is required; the
|
|
10
|
+
* remaining members are optional and emitted only when present. Either `href`
|
|
11
|
+
* (a concrete target) or `template` (a URI template) typically carries the
|
|
12
|
+
* link's value.
|
|
13
|
+
*/
|
|
14
|
+
export interface Link {
|
|
15
|
+
/** The link relation type — a registered name or an absolute URI. */
|
|
16
|
+
readonly rel: string;
|
|
17
|
+
/** Media type of the target resource, when known. */
|
|
18
|
+
readonly type?: string;
|
|
19
|
+
/** The target URI. */
|
|
20
|
+
readonly href?: string;
|
|
21
|
+
/** Human-readable titles keyed by language tag (or `"und"`). */
|
|
22
|
+
readonly titles?: Readonly<Record<string, string>>;
|
|
23
|
+
/** Additional, scheme-defined properties; a `null` value means "absent". */
|
|
24
|
+
readonly properties?: Readonly<Record<string, string | null>>;
|
|
25
|
+
/** A URI template, used in place of `href` for parameterised links. */
|
|
26
|
+
readonly template?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What the configured resource map (or {@link ResourceResolver}) yields for a
|
|
30
|
+
* controlled resource. `subject` is optional here: the handler defaults it to
|
|
31
|
+
* the queried `resource` URI (which is what Mastodon/fediverse clients require),
|
|
32
|
+
* so a record normally lists only its `aliases`, `properties`, and `links`.
|
|
33
|
+
*/
|
|
34
|
+
export interface ResourceRecord {
|
|
35
|
+
/** Overrides the echoed subject; defaults to the queried `resource` URI. */
|
|
36
|
+
readonly subject?: string;
|
|
37
|
+
/** Other URIs that identify the same entity (RFC 7033 §4.4.2). */
|
|
38
|
+
readonly aliases?: readonly string[];
|
|
39
|
+
/** Subject-level properties; a `null` value means "absent". */
|
|
40
|
+
readonly properties?: Readonly<Record<string, string | null>>;
|
|
41
|
+
/** The link relations advertised for this resource. */
|
|
42
|
+
readonly links?: readonly Link[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A fully-rendered JRD as serialised in a `200` response. `subject` and `links`
|
|
46
|
+
* are always present (`links` may be an empty array after `rel` filtering);
|
|
47
|
+
* `aliases` and `properties` appear only when the record supplied them.
|
|
48
|
+
*/
|
|
49
|
+
export interface Jrd {
|
|
50
|
+
/** The subject URI — equals the queried `resource` unless the record overrode it. */
|
|
51
|
+
readonly subject: string;
|
|
52
|
+
/** Other URIs identifying the subject. */
|
|
53
|
+
readonly aliases?: readonly string[];
|
|
54
|
+
/** Subject-level properties. */
|
|
55
|
+
readonly properties?: Readonly<Record<string, string | null>>;
|
|
56
|
+
/** The (possibly `rel`-filtered) link set. */
|
|
57
|
+
readonly links: readonly Link[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Apply the RFC 7033 §4.3 `rel` filter: with no `rel` parameters the full link
|
|
61
|
+
* set is returned; otherwise only links whose `rel` exactly matches one of the
|
|
62
|
+
* requested relations survive. `rel` filtering never touches `aliases` or
|
|
63
|
+
* subject `properties` — it scopes the `links` array only.
|
|
64
|
+
*/
|
|
65
|
+
export declare function filterLinksByRel(links: readonly Link[], rels: readonly string[]): readonly Link[];
|
|
66
|
+
/**
|
|
67
|
+
* Render the final {@link Jrd} for a matched resource: echo the queried
|
|
68
|
+
* `resource` as the subject (unless the record overrides it), apply the `rel`
|
|
69
|
+
* filter to the links, and carry through `aliases`/`properties` only when the
|
|
70
|
+
* record actually supplied them (so the JSON stays minimal).
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildJrd(resource: string, record: ResourceRecord, rels: readonly string[]): Jrd;
|
|
73
|
+
//# sourceMappingURL=jrd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jrd.d.ts","sourceRoot":"","sources":["../src/jrd.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9D,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9D,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;CAClC;AAED;;;;GAIG;AACH,MAAM,WAAW,GAAG;IAClB,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,gCAAgC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9D,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,SAAS,IAAI,EAAE,CAGjB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,GAAG,CAiBL"}
|
package/dist/jrd.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Resource Descriptor (JRD) data model and the pure `rel`-filtering rule
|
|
3
|
+
* from RFC 7033 §4.3. These types are plain data — no Workers runtime, no
|
|
4
|
+
* Cloudflare bindings — so the discovery logic unit-tests in isolation. The
|
|
5
|
+
* handler in `handler.ts` turns an HTTP request into a `resource` lookup and
|
|
6
|
+
* renders the resulting {@link Jrd} as `application/jrd+json`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Apply the RFC 7033 §4.3 `rel` filter: with no `rel` parameters the full link
|
|
10
|
+
* set is returned; otherwise only links whose `rel` exactly matches one of the
|
|
11
|
+
* requested relations survive. `rel` filtering never touches `aliases` or
|
|
12
|
+
* subject `properties` — it scopes the `links` array only.
|
|
13
|
+
*/
|
|
14
|
+
export function filterLinksByRel(links, rels) {
|
|
15
|
+
if (rels.length === 0)
|
|
16
|
+
return links;
|
|
17
|
+
return links.filter((link) => rels.includes(link.rel));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Render the final {@link Jrd} for a matched resource: echo the queried
|
|
21
|
+
* `resource` as the subject (unless the record overrides it), apply the `rel`
|
|
22
|
+
* filter to the links, and carry through `aliases`/`properties` only when the
|
|
23
|
+
* record actually supplied them (so the JSON stays minimal).
|
|
24
|
+
*/
|
|
25
|
+
export function buildJrd(resource, record, rels) {
|
|
26
|
+
const jrd = {
|
|
27
|
+
subject: record.subject ?? resource,
|
|
28
|
+
links: filterLinksByRel(record.links ?? [], rels),
|
|
29
|
+
};
|
|
30
|
+
if (record.aliases && record.aliases.length > 0) {
|
|
31
|
+
jrd.aliases = record.aliases;
|
|
32
|
+
}
|
|
33
|
+
if (record.properties && Object.keys(record.properties).length > 0) {
|
|
34
|
+
jrd.properties = record.properties;
|
|
35
|
+
}
|
|
36
|
+
return jrd;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=jrd.js.map
|
package/dist/jrd.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jrd.js","sourceRoot":"","sources":["../src/jrd.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwDH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAsB,EACtB,IAAuB;IAEvB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CACtB,QAAgB,EAChB,MAAsB,EACtB,IAAuB;IAEvB,MAAM,GAAG,GAKL;QACF,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,QAAQ;QACnC,KAAK,EAAE,gBAAgB,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,IAAI,CAAC;KAClD,CAAC;IACF,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnE,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/webfinger` — structured observability event taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* WebFinger is public discovery data, so the stakes are lower than an auth
|
|
5
|
+
* endpoint — but a server being scraped for `acct:` handles it does not control,
|
|
6
|
+
* or a misconfigured resource map that 404s every lookup, is exactly the kind of
|
|
7
|
+
* thing an operator wants a signal for. Logging and metrics are opt-in via an
|
|
8
|
+
* injected {@link Logger} and {@link Metrics} (see `@dwk/log`) and **share this
|
|
9
|
+
* one vocabulary**: the same dotted event name is passed to the logger and the
|
|
10
|
+
* metrics sink so a log line and its counter line up.
|
|
11
|
+
*
|
|
12
|
+
* Fields follow the redaction policy: a `resource` is reduced to its **host**
|
|
13
|
+
* (the domain of an `acct:`/`mailto:` handle or an `https:` URI) via the
|
|
14
|
+
* handler's `resourceHost` helper — the local part (the user identifier) is
|
|
15
|
+
* never logged, only the domain, a machine-readable `reason`, and the count of
|
|
16
|
+
* `rel` filters. See `spec/observability.md`.
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
/** Stable event names emitted by `@dwk/webfinger`. */
|
|
21
|
+
export declare const WebfingerLogEvent: {
|
|
22
|
+
/**
|
|
23
|
+
* A `resource` was matched and a JRD returned. Fields: `resourceHost`
|
|
24
|
+
* (sanitized), `relCount` (number of `rel` filters applied).
|
|
25
|
+
*/
|
|
26
|
+
readonly Resolved: "webfinger.resolved";
|
|
27
|
+
/**
|
|
28
|
+
* A request was rejected before a JRD could be returned. Field: `reason`
|
|
29
|
+
* (`missing_resource`, `malformed_resource`, `not_found`, or
|
|
30
|
+
* `method_not_allowed`), plus `resourceHost` when a `resource` was supplied.
|
|
31
|
+
*/
|
|
32
|
+
readonly Rejected: "webfinger.rejected";
|
|
33
|
+
};
|
|
34
|
+
/** Union of the event-name string literals in {@link WebfingerLogEvent}. */
|
|
35
|
+
export type WebfingerLogEvent = (typeof WebfingerLogEvent)[keyof typeof WebfingerLogEvent];
|
|
36
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,sDAAsD;AACtD,eAAO,MAAM,iBAAiB;IAC5B;;;OAGG;;IAEH;;;;OAIG;;CAEK,CAAC;AAEX,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAC3B,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC"}
|