@fedify/vocab-runtime 2.4.0-dev.2056 → 2.4.0-dev.2063

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.
@@ -7,10 +7,15 @@ import {
7
7
  import preloadedContexts from "../contexts.ts";
8
8
  import type { DocumentLoader, RemoteDocument } from "../docloader.ts";
9
9
  import jsonld from "../jsonld.ts";
10
- import type { PortableObjectVerifier } from "../portable.ts";
10
+ import type {
11
+ PortableObjectReferrer,
12
+ PortableObjectVerification,
13
+ PortableObjectVerifier,
14
+ } from "../portable.ts";
11
15
  import {
12
16
  canonicalizePortableUri,
13
17
  formatIri,
18
+ fromCompatibleEf61Id,
14
19
  parseIri,
15
20
  toCompatibleEf61Id,
16
21
  } from "../url.ts";
@@ -38,6 +43,225 @@ const BASELINE_CONTEXT_URLS: ReadonlySet<string> = new Set([
38
43
  "https://w3id.org/security/data-integrity/v1",
39
44
  ]);
40
45
 
46
+ /**
47
+ * The maximum number of links in a referrer chain passed to a
48
+ * {@link PortableObjectVerifier}. Longer chains are cut off and marked as
49
+ * truncated.
50
+ */
51
+ const MAX_REFERRER_CHAIN_LENGTH = 32;
52
+
53
+ /**
54
+ * Where a vocabulary object came from, as far as portable objects are
55
+ * concerned.
56
+ */
57
+ interface Provenance {
58
+ /**
59
+ * How the object was accepted by a portable object verifier, if it was
60
+ * returned by {@link dereferencePortableIri}.
61
+ */
62
+ readonly acceptance?: "verified" | "unsecured";
63
+
64
+ /**
65
+ * The opaque collection context that the verifier attached to the object.
66
+ */
67
+ readonly collectionContext?: unknown;
68
+
69
+ /**
70
+ * The object and property through which the object was obtained.
71
+ */
72
+ readonly referrer?: { readonly object: object; readonly property: string };
73
+ }
74
+
75
+ // Kept outside the objects so that they cannot be forged through vocabulary
76
+ // constructors or JSON-LD, and so that they do not show up in serialization:
77
+ const provenances = new WeakMap<object, Provenance>();
78
+
79
+ /**
80
+ * A reference from a vocabulary object's property, which generated
81
+ * accessors pass to {@link dereferencePortableIri}.
82
+ *
83
+ * @internal
84
+ */
85
+ export interface PortableReferrerLink {
86
+ readonly object: object;
87
+ readonly property: string;
88
+ }
89
+
90
+ function getObjectId(object: unknown): URL | null {
91
+ if (object == null || typeof object !== "object" || !("id" in object)) {
92
+ return null;
93
+ }
94
+ const id = object.id;
95
+ return id instanceof URL ? id : null;
96
+ }
97
+
98
+ /**
99
+ * Checks whether a vocabulary object is part of a chain of portable objects,
100
+ * i.e., whether it has a portable ID or was obtained from a portable object.
101
+ *
102
+ * @internal Technically exported for generated vocabulary classes, but not
103
+ * part of the public API contract. This is not considered public API for
104
+ * Semantic Versioning decisions.
105
+ */
106
+ export function isInPortableChain(object: object): boolean {
107
+ if (provenances.has(object)) return true;
108
+ const id = getObjectId(object);
109
+ return id != null && isPortableIri(id);
110
+ }
111
+
112
+ /**
113
+ * Checks whether a vocabulary object was accepted without an integrity
114
+ * proof, e.g., as an unsecured portable collection served by a trusted
115
+ * gateway. Objects embedded in such an object must not be trusted because
116
+ * of their origin.
117
+ *
118
+ * @internal Technically exported for generated vocabulary classes, but not
119
+ * part of the public API contract. This is not considered public API for
120
+ * Semantic Versioning decisions.
121
+ */
122
+ export function isUnsecuredPortableObject(object: object): boolean {
123
+ return provenances.get(object)?.acceptance === "unsecured";
124
+ }
125
+
126
+ /**
127
+ * Checks whether a URL is an FEP-ef61 compatible identifier, i.e., an HTTP(S)
128
+ * URL under a gateway's `/.well-known/apgateway/` path that stands for
129
+ * a portable object. Malformed compatible identifiers count as compatible
130
+ * identifiers too.
131
+ *
132
+ * @internal Technically exported for generated vocabulary classes, but not
133
+ * part of the public API contract. This is not considered public API for
134
+ * Semantic Versioning decisions.
135
+ */
136
+ export function isCompatibleEf61Iri(url: URL | string): boolean {
137
+ try {
138
+ return fromCompatibleEf61Id(url) != null;
139
+ } catch (error) {
140
+ if (error instanceof TypeError) return true;
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Records that a vocabulary object returned by a property accessor was
147
+ * obtained through the given property of a portable object, so that later
148
+ * dereferences from it can tell where it came from. Nothing is recorded if
149
+ * the parent is not in a portable chain, or if the child already has its
150
+ * provenance.
151
+ *
152
+ * @internal Technically exported for generated vocabulary classes, but not
153
+ * part of the public API contract. This is not considered public API for
154
+ * Semantic Versioning decisions.
155
+ */
156
+ export function recordPortableReferrer(
157
+ parent: object,
158
+ child: unknown,
159
+ property: string,
160
+ ): void {
161
+ if (child == null || typeof child !== "object") return;
162
+ if (provenances.has(child) || !isInPortableChain(parent)) return;
163
+ provenances.set(child, { referrer: { object: parent, property } });
164
+ }
165
+
166
+ /**
167
+ * Copies the provenance of a vocabulary object to its clone. The clone
168
+ * keeps where the original came from and the restriction on unsecured
169
+ * objects, but not a positive verification status nor a collection context,
170
+ * since the clone may have different property values.
171
+ *
172
+ * @internal Technically exported for generated vocabulary classes, but not
173
+ * part of the public API contract. This is not considered public API for
174
+ * Semantic Versioning decisions.
175
+ */
176
+ export function copyPortableProvenance(from: object, to: object): void {
177
+ const provenance = provenances.get(from);
178
+ if (provenance == null) return;
179
+ provenances.set(to, {
180
+ referrer: provenance.referrer,
181
+ ...(provenance.acceptance === "unsecured"
182
+ ? { acceptance: "unsecured" }
183
+ : {}),
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Rejects a reference from a portable object that would be dereferenced as
189
+ * an ordinary HTTP(S) object although it stands for a portable object, i.e.,
190
+ * an FEP-ef61 compatible identifier, or a document whose final URL or `@id`
191
+ * is a compatible identifier or a portable IRI. Such a document would skip
192
+ * the portable object policy, so it is rejected regardless of the
193
+ * `crossOrigin` option, except that `crossOrigin: "throw"` makes it throw.
194
+ *
195
+ * @param url The rejected URL, for the log message.
196
+ * @param crossOrigin The `crossOrigin` option of the accessor.
197
+ * @returns Always `null`.
198
+ * @throws {Error} If `crossOrigin` is `"throw"`.
199
+ * @internal Technically exported for generated vocabulary classes, but not
200
+ * part of the public API contract. This is not considered public API for
201
+ * Semantic Versioning decisions.
202
+ */
203
+ export function rejectPortableChainReference(
204
+ url: string,
205
+ crossOrigin: "ignore" | "throw" | "trust" | undefined,
206
+ ): null {
207
+ const message = "Refusing to dereference {url} from a portable object as " +
208
+ "an ordinary HTTP(S) object, because it stands for an FEP-ef61 portable " +
209
+ "object that is not verified this way.";
210
+ if (crossOrigin === "throw") {
211
+ throw new Error(message.replace("{url}", url));
212
+ }
213
+ logger.warn(message, { url });
214
+ return null;
215
+ }
216
+
217
+ /**
218
+ * Logs that an embedded object without `@id` in an object accepted without
219
+ * an integrity proof is dropped, because it cannot be dereferenced and
220
+ * verified on its own.
221
+ *
222
+ * @internal Technically exported for generated vocabulary classes, but not
223
+ * part of the public API contract. This is not considered public API for
224
+ * Semantic Versioning decisions.
225
+ */
226
+ export function warnUnverifiableEmbeddedObject(
227
+ parent: object,
228
+ property: string,
229
+ ): void {
230
+ logger.warn(
231
+ "Dropping an embedded object without @id in the {property} property of " +
232
+ "{parentId}, because the parent was accepted without an integrity " +
233
+ "proof and the embedded object cannot be verified on its own.",
234
+ { property, parentId: getObjectId(parent)?.href ?? null },
235
+ );
236
+ }
237
+
238
+ function buildReferrerChain(
239
+ link: PortableReferrerLink | undefined,
240
+ depth = 0,
241
+ ): PortableObjectReferrer | undefined {
242
+ if (link == null) return undefined;
243
+ const provenance = provenances.get(link.object);
244
+ const base = {
245
+ object: link.object,
246
+ id: getObjectId(link.object),
247
+ property: link.property,
248
+ ...(provenance?.acceptance == null
249
+ ? {}
250
+ : { acceptance: provenance.acceptance }),
251
+ ...(provenance?.collectionContext === undefined
252
+ ? {}
253
+ : { collectionContext: provenance.collectionContext }),
254
+ };
255
+ if (provenance?.referrer == null) return base;
256
+ if (depth + 1 >= MAX_REFERRER_CHAIN_LENGTH) {
257
+ return { ...base, truncated: true };
258
+ }
259
+ return {
260
+ ...base,
261
+ referrer: buildReferrerChain(provenance.referrer, depth + 1),
262
+ };
263
+ }
264
+
41
265
  /**
42
266
  * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
43
267
  *
@@ -179,6 +403,7 @@ export interface DereferencePortableIriOptions<T> {
179
403
  tracerProvider: TracerProvider;
180
404
  gateways?: readonly (string | URL)[];
181
405
  verifyPortableObject?: PortableObjectVerifier;
406
+ referrer?: PortableReferrerLink;
182
407
  suppressError?: boolean;
183
408
  crossOrigin?: "ignore" | "throw" | "trust";
184
409
  /**
@@ -287,6 +512,11 @@ export async function dereferencePortableIri<T extends { id: URL | null }>(
287
512
  const contextLoader = snapshot.loader;
288
513
  const attempts: Attempt[] = [];
289
514
  const { signal } = options;
515
+ const explicitGateways = options.gateways == null ? undefined : gateways;
516
+ const gatewayHints = options.gateways == null && gateways.length > 0
517
+ ? gateways
518
+ : undefined;
519
+ const referrer = buildReferrerChain(options.referrer);
290
520
  try {
291
521
  for (const { url: requestUrl, gateway } of requestUrls) {
292
522
  signal?.throwIfAborted();
@@ -309,12 +539,22 @@ export async function dereferencePortableIri<T extends { id: URL | null }>(
309
539
  const { document } = remoteDocument;
310
540
  try {
311
541
  await checkPortableObjectId(document, expectedId, contextLoader);
312
- let result: { readonly verified: boolean };
542
+ let documentUrl: URL | undefined;
543
+ try {
544
+ documentUrl = parseIri(remoteDocument.documentUrl);
545
+ } catch {
546
+ documentUrl = undefined;
547
+ }
548
+ let result: PortableObjectVerification;
313
549
  try {
314
550
  result = await verify(document, {
315
551
  documentLoader: options.documentLoader,
316
552
  contextLoader,
317
553
  tracerProvider: options.tracerProvider,
554
+ ...(documentUrl == null ? {} : { documentUrl }),
555
+ ...(explicitGateways == null ? {} : { gateways: explicitGateways }),
556
+ ...(gatewayHints == null ? {} : { gatewayHints }),
557
+ ...(referrer == null ? {} : { referrer }),
318
558
  });
319
559
  } catch (error) {
320
560
  throw new PortableObjectRejection(
@@ -333,6 +573,15 @@ export async function dereferencePortableIri<T extends { id: URL | null }>(
333
573
  baseUrl: url,
334
574
  });
335
575
  signal?.throwIfAborted();
576
+ if (object != null && typeof object === "object") {
577
+ provenances.set(object, {
578
+ acceptance: result.unsecured === true ? "unsecured" : "verified",
579
+ ...(result.collectionContext === undefined
580
+ ? {}
581
+ : { collectionContext: result.collectionContext }),
582
+ ...(options.referrer == null ? {} : { referrer: options.referrer }),
583
+ });
584
+ }
336
585
  if (gateway != null) {
337
586
  span?.setAttribute("activitypub.gateway", gateway.href);
338
587
  }
package/src/mod.ts CHANGED
@@ -46,6 +46,8 @@ export {
46
46
  } from "./digest.ts";
47
47
  export { LanguageString } from "./langstr.ts";
48
48
  export type {
49
+ PortableObjectReferrer,
50
+ PortableObjectVerification,
49
51
  PortableObjectVerifier,
50
52
  PortableObjectVerifierOptions,
51
53
  } from "./portable.ts";
package/src/portable.ts CHANGED
@@ -23,30 +23,154 @@ export interface PortableObjectVerifierOptions {
23
23
  * The OpenTelemetry tracer provider.
24
24
  */
25
25
  readonly tracerProvider?: TracerProvider;
26
+
27
+ /**
28
+ * The URL the document was finally retrieved from, i.e., the `documentUrl`
29
+ * of the document loader's result, which may differ from the requested URL
30
+ * after redirects. Trust decisions such as the [FEP-ef61] gateway trust
31
+ * policy for unsecured collections depend on it, so custom document loaders
32
+ * must report it truthfully.
33
+ *
34
+ * [FEP-ef61]: https://w3id.org/fep/ef61
35
+ * @since 2.4.0
36
+ */
37
+ readonly documentUrl?: URL;
38
+
39
+ /**
40
+ * The gateways that the caller explicitly passed to the accessor through
41
+ * its `gateways` option, in order. It is `undefined` if the caller did not
42
+ * pass the option, and can be empty.
43
+ * @since 2.4.0
44
+ */
45
+ readonly gateways?: readonly URL[];
46
+
47
+ /**
48
+ * The gateways taken from the `@gateway` location hints of the reference,
49
+ * in order, when the caller did not pass the `gateways` option.
50
+ * @since 2.4.0
51
+ */
52
+ readonly gatewayHints?: readonly URL[];
53
+
54
+ /**
55
+ * The object whose property referred to the document, if the document was
56
+ * dereferenced through a property accessor.
57
+ * @since 2.4.0
58
+ */
59
+ readonly referrer?: PortableObjectReferrer;
26
60
  }
27
61
 
28
62
  /**
29
- * A function that applies the [FEP-ef61] proof policy to a portable object
63
+ * An object whose property referred to a portable object being verified.
64
+ * Referrers form a chain: {@link PortableObjectReferrer.referrer} is the
65
+ * object through which this referrer itself was obtained, and so on.
66
+ * @since 2.4.0
67
+ */
68
+ export interface PortableObjectReferrer {
69
+ /**
70
+ * The referring vocabulary object.
71
+ */
72
+ readonly object: unknown;
73
+
74
+ /**
75
+ * The `@id` of the referring object, if any.
76
+ */
77
+ readonly id: URL | null;
78
+
79
+ /**
80
+ * The expanded URI of the property through which the referring object
81
+ * referred to the next object in the chain, e.g.,
82
+ * `https://www.w3.org/ns/activitystreams#outbox`.
83
+ */
84
+ readonly property: string;
85
+
86
+ /**
87
+ * How the referring object itself was accepted by a
88
+ * {@link PortableObjectVerifier} when it was dereferenced through gateways:
89
+ *
90
+ * - `"verified"`: accepted as verified, e.g., with a valid integrity proof.
91
+ * - `"unsecured"`: accepted without an integrity proof, e.g., as an
92
+ * unsecured collection served by a trusted gateway.
93
+ *
94
+ * It is `undefined` if the referring object was not dereferenced through
95
+ * gateways, e.g., if it was embedded in another object or constructed by
96
+ * the application.
97
+ */
98
+ readonly acceptance?: "verified" | "unsecured";
99
+
100
+ /**
101
+ * The opaque collection context that a {@link PortableObjectVerifier}
102
+ * attached to the referring object when it accepted it. See
103
+ * {@link PortableObjectVerification}.
104
+ */
105
+ readonly collectionContext?: unknown;
106
+
107
+ /**
108
+ * The referrer of the referring object, if any.
109
+ */
110
+ readonly referrer?: PortableObjectReferrer;
111
+
112
+ /**
113
+ * Whether the chain was cut off here because it was too long. If `true`,
114
+ * the referring object may have a referrer which is not included.
115
+ */
116
+ readonly truncated?: boolean;
117
+ }
118
+
119
+ /**
120
+ * The result of a {@link PortableObjectVerifier}.
121
+ * @since 2.4.0
122
+ */
123
+ export type PortableObjectVerification =
124
+ | {
125
+ /** Whether the document is accepted. */
126
+ readonly verified: false;
127
+ }
128
+ | {
129
+ /** Whether the document is accepted. */
130
+ readonly verified: true;
131
+
132
+ /**
133
+ * Whether the document was accepted without an integrity proof, e.g., as
134
+ * an unsecured [FEP-ef61] collection served by a gateway that its owner
135
+ * lists. Objects embedded in such a document are not trusted because of
136
+ * their origin; property accessors dereference and verify them one by
137
+ * one instead.
138
+ *
139
+ * [FEP-ef61]: https://w3id.org/fep/ef61
140
+ */
141
+ readonly unsecured?: boolean;
142
+
143
+ /**
144
+ * An opaque value that the verifier wants to receive again as
145
+ * {@link PortableObjectReferrer.collectionContext} when objects referred
146
+ * to by this document, such as collection pages, are verified.
147
+ */
148
+ readonly collectionContext?: unknown;
149
+ };
150
+
151
+ /**
152
+ * A function that applies the [FEP-ef61] trust policy to a portable object
30
153
  * fetched through a gateway.
31
154
  *
32
155
  * It receives the fetched JSON-LD document as is, and resolves to an object
33
- * whose `verified` property tells whether the document carries a valid
34
- * [FEP-8b32] Object Integrity Proof made by the DID in its portable ID.
35
- * `verifyPortableObjectProof()` from `@fedify/fedify` satisfies this type, so
36
- * it can be passed as the `verifyPortableObject` option of generated
156
+ * whose `verified` property tells whether the document is acceptable, e.g.,
157
+ * whether it carries a valid [FEP-8b32] Object Integrity Proof made by the
158
+ * DID in its portable ID. `verifyPortableObject()` and
159
+ * `verifyPortableObjectProof()` from `@fedify/fedify` satisfy this type, so
160
+ * they can be passed as the `verifyPortableObject` option of generated
37
161
  * vocabulary accessors such as `Activity.getObject()`.
38
162
  *
39
163
  * [FEP-ef61]: https://w3id.org/fep/ef61
40
164
  * [FEP-8b32]: https://w3id.org/fep/8b32
41
165
  *
42
166
  * @param document The fetched JSON-LD document.
43
- * @param options Loaders and tracing options for verification.
167
+ * @param options Loaders, tracing options, and where the document came from.
44
168
  * @returns An object whose `verified` property is `true` if and only if the
45
- * document satisfies the proof policy. Other properties, such as
169
+ * document satisfies the policy. Other properties, such as
46
170
  * a failure reason, are logged when verification fails.
47
171
  * @since 2.4.0
48
172
  */
49
173
  export type PortableObjectVerifier = (
50
174
  document: unknown,
51
175
  options: PortableObjectVerifierOptions,
52
- ) => Promise<{ readonly verified: boolean }>;
176
+ ) => Promise<PortableObjectVerification>;
@@ -1,52 +0,0 @@
1
- /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader } from "./docloader-Dr8wEkif.js";
3
- import { TracerProvider } from "@opentelemetry/api";
4
-
5
- //#region src/portable.d.ts
6
- /**
7
- * Options passed to a {@link PortableObjectVerifier}.
8
- * @since 2.4.0
9
- */
10
- interface PortableObjectVerifierOptions {
11
- /**
12
- * The document loader for fetching remote documents, such as verification
13
- * methods that are not `did:key` URLs.
14
- */
15
- readonly documentLoader?: DocumentLoader;
16
- /**
17
- * The document loader for fetching remote JSON-LD contexts. During
18
- * gateway dereferencing, this loader returns the same context documents
19
- * that the vocabulary parser sees.
20
- */
21
- readonly contextLoader?: DocumentLoader;
22
- /**
23
- * The OpenTelemetry tracer provider.
24
- */
25
- readonly tracerProvider?: TracerProvider;
26
- }
27
- /**
28
- * A function that applies the [FEP-ef61] proof policy to a portable object
29
- * fetched through a gateway.
30
- *
31
- * It receives the fetched JSON-LD document as is, and resolves to an object
32
- * whose `verified` property tells whether the document carries a valid
33
- * [FEP-8b32] Object Integrity Proof made by the DID in its portable ID.
34
- * `verifyPortableObjectProof()` from `@fedify/fedify` satisfies this type, so
35
- * it can be passed as the `verifyPortableObject` option of generated
36
- * vocabulary accessors such as `Activity.getObject()`.
37
- *
38
- * [FEP-ef61]: https://w3id.org/fep/ef61
39
- * [FEP-8b32]: https://w3id.org/fep/8b32
40
- *
41
- * @param document The fetched JSON-LD document.
42
- * @param options Loaders and tracing options for verification.
43
- * @returns An object whose `verified` property is `true` if and only if the
44
- * document satisfies the proof policy. Other properties, such as
45
- * a failure reason, are logged when verification fails.
46
- * @since 2.4.0
47
- */
48
- type PortableObjectVerifier = (document: unknown, options: PortableObjectVerifierOptions) => Promise<{
49
- readonly verified: boolean;
50
- }>;
51
- //#endregion
52
- export { PortableObjectVerifierOptions as n, PortableObjectVerifier as t };
@@ -1,52 +0,0 @@
1
- /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader } from "./docloader-Dr8wEkif.cjs";
3
- import { TracerProvider } from "@opentelemetry/api";
4
-
5
- //#region src/portable.d.ts
6
- /**
7
- * Options passed to a {@link PortableObjectVerifier}.
8
- * @since 2.4.0
9
- */
10
- interface PortableObjectVerifierOptions {
11
- /**
12
- * The document loader for fetching remote documents, such as verification
13
- * methods that are not `did:key` URLs.
14
- */
15
- readonly documentLoader?: DocumentLoader;
16
- /**
17
- * The document loader for fetching remote JSON-LD contexts. During
18
- * gateway dereferencing, this loader returns the same context documents
19
- * that the vocabulary parser sees.
20
- */
21
- readonly contextLoader?: DocumentLoader;
22
- /**
23
- * The OpenTelemetry tracer provider.
24
- */
25
- readonly tracerProvider?: TracerProvider;
26
- }
27
- /**
28
- * A function that applies the [FEP-ef61] proof policy to a portable object
29
- * fetched through a gateway.
30
- *
31
- * It receives the fetched JSON-LD document as is, and resolves to an object
32
- * whose `verified` property tells whether the document carries a valid
33
- * [FEP-8b32] Object Integrity Proof made by the DID in its portable ID.
34
- * `verifyPortableObjectProof()` from `@fedify/fedify` satisfies this type, so
35
- * it can be passed as the `verifyPortableObject` option of generated
36
- * vocabulary accessors such as `Activity.getObject()`.
37
- *
38
- * [FEP-ef61]: https://w3id.org/fep/ef61
39
- * [FEP-8b32]: https://w3id.org/fep/8b32
40
- *
41
- * @param document The fetched JSON-LD document.
42
- * @param options Loaders and tracing options for verification.
43
- * @returns An object whose `verified` property is `true` if and only if the
44
- * document satisfies the proof policy. Other properties, such as
45
- * a failure reason, are logged when verification fails.
46
- * @since 2.4.0
47
- */
48
- type PortableObjectVerifier = (document: unknown, options: PortableObjectVerifierOptions) => Promise<{
49
- readonly verified: boolean;
50
- }>;
51
- //#endregion
52
- export { PortableObjectVerifierOptions as n, PortableObjectVerifier as t };