@fedify/vocab-tools 2.4.0-dev.2056 → 2.4.0-dev.2065

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/src/class.ts CHANGED
@@ -19,8 +19,21 @@ const INTERNAL_RUNTIME_IMPORTS = [
19
19
  "normalizeJsonLdIris",
20
20
  ].join(",\n ");
21
21
  const PORTABLE_DEREFERENCE_IMPORTS = [
22
+ "copyPortableProvenance",
23
+ "createSnapshotContextLoader",
22
24
  "dereferencePortableIri",
25
+ "getPortableResponseClaim",
23
26
  "isPortableIri",
27
+ "isPortableMode",
28
+ "isUnsecuredPortableObject",
29
+ "isUnverifiedPortableClaim",
30
+ "markUnverifiedPortableClaim",
31
+ "mustDereferencePortableObject",
32
+ "parseCompatibleEf61Reference",
33
+ "type PortableResponseClaim",
34
+ "recordPortableReferrer",
35
+ "rejectMalformedCompatibleReference",
36
+ "warnUnverifiableEmbeddedObject",
24
37
  ].join(",\n ");
25
38
  const SIGNED_REPRESENTATION_IMPORTS = [
26
39
  "enterSignedValueScope",
@@ -246,6 +246,7 @@ export async function* generateCloner(
246
246
  { id: values.id ?? this.id },
247
247
  options
248
248
  );
249
+ copyPortableProvenance(this, clone);
249
250
  `;
250
251
  } else {
251
252
  yield `const clone = super.clone(values, options) as unknown as ${type.name};`;
package/src/property.ts CHANGED
@@ -89,14 +89,27 @@ async function* generateProperty(
89
89
  ${JSON.stringify(metadata.version)},
90
90
  );
91
91
  return await tracer.startActiveSpan("activitypub.lookup_object", async (span) => {
92
- if (isPortableIri(url)) {
92
+ const dereferencePortable = async (
93
+ portableUrl: URL,
94
+ extra: {
95
+ inferredGateways?: readonly URL[];
96
+ response?: RemoteDocument;
97
+ contextLoader?: DocumentLoader;
98
+ } = {},
99
+ ) => {
93
100
  try {
94
- const obj = await dereferencePortableIri(url, {
101
+ const obj = await dereferencePortableIri(portableUrl, {
95
102
  documentLoader,
96
- contextLoader,
103
+ contextLoader: extra.contextLoader ?? contextLoader,
97
104
  tracerProvider,
98
105
  gateways: options.gateways,
106
+ inferredGateways: extra.inferredGateways,
107
+ response: extra.response,
99
108
  verifyPortableObject: options.verifyPortableObject,
109
+ referrer: {
110
+ object: this,
111
+ property: ${JSON.stringify(property.uri)},
112
+ },
100
113
  suppressError: options.suppressError,
101
114
  crossOrigin: options.crossOrigin,
102
115
  span,
@@ -109,7 +122,10 @@ async function* generateProperty(
109
122
  }),
110
123
  });
111
124
  if (obj != null) {
112
- span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
125
+ span.setAttribute(
126
+ "activitypub.object.id",
127
+ (obj.id ?? portableUrl).href,
128
+ );
113
129
  span.setAttribute(
114
130
  "activitypub.object.type",
115
131
  // @ts-ignore: obj.constructor always has a typeId.
@@ -126,8 +142,21 @@ async function* generateProperty(
126
142
  } finally {
127
143
  span.end();
128
144
  }
129
- }
145
+ };
146
+ if (isPortableIri(url)) return await dereferencePortable(url);
130
147
  const lookupUrl = formatIri(url);
148
+ const portableMode = isPortableMode(this, options);
149
+ if (portableMode) {
150
+ const compatible = parseCompatibleEf61Reference(url);
151
+ if (compatible === null) {
152
+ span.end();
153
+ return rejectMalformedCompatibleReference(lookupUrl, options);
154
+ } else if (compatible != null) {
155
+ return await dereferencePortable(compatible.id, {
156
+ inferredGateways: [compatible.gateway],
157
+ });
158
+ }
159
+ }
131
160
  let fetchResult: RemoteDocument;
132
161
  try {
133
162
  fetchResult = await documentLoader(lookupUrl);
@@ -147,52 +176,90 @@ async function* generateProperty(
147
176
  throw error;
148
177
  }
149
178
  const { document, documentUrl } = fetchResult;
150
- const baseUrl = parseIri(documentUrl);
179
+ // In portable mode, the document may turn out to be a portable object,
180
+ // which has to be verified with the same context documents:
181
+ const snapshot = portableMode
182
+ ? createSnapshotContextLoader(contextLoader)
183
+ : null;
151
184
  try {
152
- const obj = await this.#${property.singularName}_fromJsonLd(
153
- document,
154
- { documentLoader, contextLoader, tracerProvider, baseUrl }
155
- );
156
- if (obj?.id != null && !isTrustedIriOrigin(options, obj.id, baseUrl)) {
157
- if (options.crossOrigin === "throw") {
158
- throw new Error(
159
- "The object's @id (" + obj.id.href + ") has a different origin " +
160
- "than the document URL (" + baseUrl.href + "); refusing to return " +
161
- "the object. If you want to bypass this check and are aware of" +
162
- 'the security implications, set the crossOrigin option to "trust".'
185
+ let claim: PortableResponseClaim | null | undefined;
186
+ try {
187
+ const baseUrl = parseIri(documentUrl);
188
+ const obj = await this.#${property.singularName}_fromJsonLd(
189
+ document,
190
+ {
191
+ documentLoader,
192
+ contextLoader: snapshot?.loader ?? contextLoader,
193
+ tracerProvider,
194
+ baseUrl,
195
+ }
196
+ );
197
+ if (snapshot == null) {
198
+ markUnverifiedPortableClaim(obj, url, documentUrl);
199
+ } else {
200
+ claim = getPortableResponseClaim(documentUrl, obj.id);
201
+ }
202
+ if (claim === undefined) {
203
+ if (
204
+ obj?.id != null &&
205
+ !isTrustedIriOrigin(options, obj.id, baseUrl)
206
+ ) {
207
+ if (options.crossOrigin === "throw") {
208
+ throw new Error(
209
+ "The object's @id (" + obj.id.href + ") has a different origin " +
210
+ "than the document URL (" + baseUrl.href + "); refusing to return " +
211
+ "the object. If you want to bypass this check and are aware of" +
212
+ 'the security implications, set the crossOrigin option to "trust".'
213
+ );
214
+ }
215
+ getLogger(["fedify", "vocab"]).warn(
216
+ "The object's @id ({objectId}) has a different origin than the document " +
217
+ "URL ({documentUrl}); refusing to return the object. If you want to " +
218
+ "bypass this check and are aware of the security implications, " +
219
+ 'set the crossOrigin option to "trust".',
220
+ { ...fetchResult, objectId: obj.id.href },
221
+ );
222
+ span.end();
223
+ return null;
224
+ }
225
+ span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
226
+ span.setAttribute(
227
+ "activitypub.object.type",
228
+ // @ts-ignore: obj.constructor always has a typeId.
229
+ obj.constructor.typeId.href
163
230
  );
231
+ span.end();
232
+ return obj;
164
233
  }
165
- getLogger(["fedify", "vocab"]).warn(
166
- "The object's @id ({objectId}) has a different origin than the document " +
167
- "URL ({documentUrl}); refusing to return the object. If you want to " +
168
- "bypass this check and are aware of the security implications, " +
169
- 'set the crossOrigin option to "trust".',
170
- { ...fetchResult, objectId: obj.id.href },
171
- );
172
- return null;
234
+ } catch (e) {
235
+ if (options.suppressError) {
236
+ getLogger(["fedify", "vocab"]).error(
237
+ "Failed to parse {url}: {error}",
238
+ { error: e, url: lookupUrl }
239
+ );
240
+ span.end();
241
+ return null;
242
+ }
243
+ span.setStatus({
244
+ code: SpanStatusCode.ERROR,
245
+ message: String(e),
246
+ });
247
+ span.end();
248
+ throw e;
173
249
  }
174
- span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
175
- span.setAttribute(
176
- "activitypub.object.type",
177
- // @ts-ignore: obj.constructor always has a typeId.
178
- obj.constructor.typeId.href
179
- );
180
- return obj;
181
- } catch (e) {
182
- if (options.suppressError) {
183
- getLogger(["fedify", "vocab"]).error(
184
- "Failed to parse {url}: {error}",
185
- { error: e, url: lookupUrl }
186
- );
187
- return null;
250
+ // The document stands for a portable object, so it is verified as
251
+ // one instead of being trusted because of where it came from:
252
+ if (claim === null) {
253
+ span.end();
254
+ return rejectMalformedCompatibleReference(documentUrl, options);
188
255
  }
189
- span.setStatus({
190
- code: SpanStatusCode.ERROR,
191
- message: String(e),
256
+ return await dereferencePortable(claim.id, {
257
+ inferredGateways: claim.inferredGateways,
258
+ response: fetchResult,
259
+ contextLoader: snapshot?.loader,
192
260
  });
193
- throw e;
194
261
  } finally {
195
- span.end();
262
+ snapshot?.release();
196
263
  }
197
264
  });
198
265
  }
@@ -319,18 +386,35 @@ async function* generateProperty(
319
386
  if (this.${await getFieldName(property.uri)}.length < 1) return null;
320
387
  let v = this.${await getFieldName(property.uri)}[0];
321
388
  if (!(v instanceof URL) &&
322
- v.id != null &&
323
- !isTrustedIriOrigin(options, v.id, ${trustOwner}) &&
324
389
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
325
- v = v.id;
390
+ if (v.id == null) {
391
+ if (isUnsecuredPortableObject(this)) {
392
+ warnUnverifiableEmbeddedObject(this, ${
393
+ JSON.stringify(property.uri)
394
+ });
395
+ return null;
396
+ }
397
+ } else if (
398
+ !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
399
+ isUnsecuredPortableObject(this) ||
400
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
401
+ ) {
402
+ v = v.id;
403
+ }
326
404
  }
327
405
  if (v instanceof URL) {
328
406
  const fetched =
329
407
  await this.#fetch${pascalCase(property.singularName)}(v, options);
330
408
  if (fetched == null) return null;
409
+ // Not cached, so that it is verified when the portable object
410
+ // policy applies later:
411
+ if (isUnverifiedPortableClaim(fetched)) return fetched;
331
412
  this.${await getFieldName(property.uri)}[0] = fetched;
332
413
  this.${await getFieldName(property.uri, "#_trust")}.add(0);
333
414
  this._cachedJsonLd = undefined;
415
+ recordPortableReferrer(this, fetched, ${
416
+ JSON.stringify(property.uri)
417
+ });
334
418
  return fetched;
335
419
  }
336
420
  `;
@@ -360,9 +444,13 @@ async function* generateProperty(
360
444
  }
361
445
  yield `
362
446
  if (v?.id != null &&
363
- ${
447
+ ((${
364
448
  type.trustEmbeddedObjects === false ? "" : "this.id != null && "
365
- }!isTrustedIriOrigin(options, v.id, ${trustOwner}) &&
449
+ }!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
450
+ isUnsecuredPortableObject(this) ||
451
+ mustDereferencePortableObject(
452
+ this, ${trustOwner}, v.id, options,
453
+ )) &&
366
454
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
367
455
  if (options.crossOrigin === "throw") {
368
456
  throw new Error(
@@ -370,7 +458,7 @@ async function* generateProperty(
370
458
  "origin than the property owner's @id (" + ${
371
459
  type.trustEmbeddedObjects === false
372
460
  ? '"untrusted metadata"'
373
- : "this.id.href"
461
+ : "(this.id?.href ?? null)"
374
462
  } + "); " +
375
463
  "refusing to return the object. If you want to bypass this " +
376
464
  "check and are aware of the security implications, set the " +
@@ -386,11 +474,12 @@ async function* generateProperty(
386
474
  { objectId: v.id.href, parentObjectId: ${
387
475
  type.trustEmbeddedObjects === false
388
476
  ? '"untrusted metadata"'
389
- : "this.id.href"
477
+ : "(this.id?.href ?? null)"
390
478
  } },
391
479
  );
392
480
  return null;
393
481
  }
482
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
394
483
  return v;
395
484
  }
396
485
  `;
@@ -437,18 +526,38 @@ async function* generateProperty(
437
526
  for (let i = 0; i < vs.length; i++) {
438
527
  let v = vs[i];
439
528
  if (!(v instanceof URL) &&
440
- v.id != null &&
441
- !isTrustedIriOrigin(options, v.id, ${trustOwner}) &&
442
529
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
443
- v = v.id;
530
+ if (v.id == null) {
531
+ if (isUnsecuredPortableObject(this)) {
532
+ warnUnverifiableEmbeddedObject(this, ${
533
+ JSON.stringify(property.uri)
534
+ });
535
+ continue;
536
+ }
537
+ } else if (
538
+ !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
539
+ isUnsecuredPortableObject(this) ||
540
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
541
+ ) {
542
+ v = v.id;
543
+ }
444
544
  }
445
545
  if (v instanceof URL) {
446
546
  const fetched =
447
547
  await this.#fetch${pascalCase(property.singularName)}(v, options);
448
548
  if (fetched == null) continue;
549
+ // Not cached, so that it is verified when the portable object
550
+ // policy applies later:
551
+ if (isUnverifiedPortableClaim(fetched)) {
552
+ yield fetched;
553
+ continue;
554
+ }
449
555
  vs[i] = fetched;
450
556
  this.${await getFieldName(property.uri, "#_trust")}.add(i);
451
557
  this._cachedJsonLd = undefined;
558
+ recordPortableReferrer(this, fetched, ${
559
+ JSON.stringify(property.uri)
560
+ });
452
561
  yield fetched;
453
562
  continue;
454
563
  }
@@ -479,9 +588,13 @@ async function* generateProperty(
479
588
  }
480
589
  yield `
481
590
  if (v?.id != null &&
482
- ${
591
+ ((${
483
592
  type.trustEmbeddedObjects === false ? "" : "this.id != null && "
484
- }!isTrustedIriOrigin(options, v.id, ${trustOwner}) &&
593
+ }!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
594
+ isUnsecuredPortableObject(this) ||
595
+ mustDereferencePortableObject(
596
+ this, ${trustOwner}, v.id, options,
597
+ )) &&
485
598
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
486
599
  if (options.crossOrigin === "throw") {
487
600
  throw new Error(
@@ -489,7 +602,7 @@ async function* generateProperty(
489
602
  "origin than the property owner's @id (" + ${
490
603
  type.trustEmbeddedObjects === false
491
604
  ? '"untrusted metadata"'
492
- : "this.id.href"
605
+ : "(this.id?.href ?? null)"
493
606
  } + "); " +
494
607
  "refusing to return the object. If you want to bypass this " +
495
608
  "check and are aware of the security implications, set the " +
@@ -505,11 +618,12 @@ async function* generateProperty(
505
618
  { objectId: v.id.href, parentObjectId: ${
506
619
  type.trustEmbeddedObjects === false
507
620
  ? '"untrusted metadata"'
508
- : "this.id.href"
621
+ : "(this.id?.href ?? null)"
509
622
  } },
510
623
  );
511
624
  continue;
512
625
  }
626
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
513
627
  yield v;
514
628
  }
515
629
  }