@fedify/vocab-tools 2.4.0-dev.2063 → 2.4.0-dev.2071

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
@@ -20,13 +20,19 @@ const INTERNAL_RUNTIME_IMPORTS = [
20
20
  ].join(",\n ");
21
21
  const PORTABLE_DEREFERENCE_IMPORTS = [
22
22
  "copyPortableProvenance",
23
+ "createSnapshotContextLoader",
23
24
  "dereferencePortableIri",
24
- "isCompatibleEf61Iri",
25
- "isInPortableChain",
25
+ "getPortableResponseClaim",
26
26
  "isPortableIri",
27
+ "isPortableMode",
27
28
  "isUnsecuredPortableObject",
29
+ "isUnverifiedPortableClaim",
30
+ "markUnverifiedPortableClaim",
31
+ "mustDereferencePortableObject",
32
+ "parseCompatibleEf61Reference",
33
+ "type PortableResponseClaim",
28
34
  "recordPortableReferrer",
29
- "rejectPortableChainReference",
35
+ "rejectMalformedCompatibleReference",
30
36
  "warnUnverifiableEmbeddedObject",
31
37
  ].join(",\n ");
32
38
  const SIGNED_REPRESENTATION_IMPORTS = [
package/src/property.ts CHANGED
@@ -89,13 +89,22 @@ 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,
100
109
  referrer: {
101
110
  object: this,
@@ -113,7 +122,10 @@ async function* generateProperty(
113
122
  }),
114
123
  });
115
124
  if (obj != null) {
116
- span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
125
+ span.setAttribute(
126
+ "activitypub.object.id",
127
+ (obj.id ?? portableUrl).href,
128
+ );
117
129
  span.setAttribute(
118
130
  "activitypub.object.type",
119
131
  // @ts-ignore: obj.constructor always has a typeId.
@@ -130,12 +142,20 @@ async function* generateProperty(
130
142
  } finally {
131
143
  span.end();
132
144
  }
133
- }
145
+ };
146
+ if (isPortableIri(url)) return await dereferencePortable(url);
134
147
  const lookupUrl = formatIri(url);
135
- const inPortableChain = isInPortableChain(this);
136
- if (inPortableChain && isCompatibleEf61Iri(url)) {
137
- span.end();
138
- return rejectPortableChainReference(lookupUrl, options.crossOrigin);
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
+ }
139
159
  }
140
160
  let fetchResult: RemoteDocument;
141
161
  try {
@@ -156,63 +176,90 @@ async function* generateProperty(
156
176
  throw error;
157
177
  }
158
178
  const { document, documentUrl } = fetchResult;
159
- 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;
160
184
  try {
161
- const obj = await this.#${property.singularName}_fromJsonLd(
162
- document,
163
- { documentLoader, contextLoader, tracerProvider, baseUrl }
164
- );
165
- if (
166
- inPortableChain &&
167
- (isCompatibleEf61Iri(documentUrl) ||
168
- (obj.id != null &&
169
- (isPortableIri(obj.id) || isCompatibleEf61Iri(obj.id))))
170
- ) {
171
- return rejectPortableChainReference(
172
- documentUrl,
173
- options.crossOrigin,
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
+ }
174
196
  );
175
- }
176
- if (obj?.id != null && !isTrustedIriOrigin(options, obj.id, baseUrl)) {
177
- if (options.crossOrigin === "throw") {
178
- throw new Error(
179
- "The object's @id (" + obj.id.href + ") has a different origin " +
180
- "than the document URL (" + baseUrl.href + "); refusing to return " +
181
- "the object. If you want to bypass this check and are aware of" +
182
- 'the security implications, set the crossOrigin option to "trust".'
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
183
230
  );
231
+ span.end();
232
+ return obj;
184
233
  }
185
- getLogger(["fedify", "vocab"]).warn(
186
- "The object's @id ({objectId}) has a different origin than the document " +
187
- "URL ({documentUrl}); refusing to return the object. If you want to " +
188
- "bypass this check and are aware of the security implications, " +
189
- 'set the crossOrigin option to "trust".',
190
- { ...fetchResult, objectId: obj.id.href },
191
- );
192
- 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;
193
249
  }
194
- span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
195
- span.setAttribute(
196
- "activitypub.object.type",
197
- // @ts-ignore: obj.constructor always has a typeId.
198
- obj.constructor.typeId.href
199
- );
200
- return obj;
201
- } catch (e) {
202
- if (options.suppressError) {
203
- getLogger(["fedify", "vocab"]).error(
204
- "Failed to parse {url}: {error}",
205
- { error: e, url: lookupUrl }
206
- );
207
- 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);
208
255
  }
209
- span.setStatus({
210
- code: SpanStatusCode.ERROR,
211
- message: String(e),
256
+ return await dereferencePortable(claim.id, {
257
+ inferredGateways: claim.inferredGateways,
258
+ response: fetchResult,
259
+ contextLoader: snapshot?.loader,
212
260
  });
213
- throw e;
214
261
  } finally {
215
- span.end();
262
+ snapshot?.release();
216
263
  }
217
264
  });
218
265
  }
@@ -350,7 +397,7 @@ async function* generateProperty(
350
397
  } else if (
351
398
  !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
352
399
  isUnsecuredPortableObject(this) ||
353
- (isInPortableChain(this) && isCompatibleEf61Iri(v.id))
400
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
354
401
  ) {
355
402
  v = v.id;
356
403
  }
@@ -359,6 +406,9 @@ async function* generateProperty(
359
406
  const fetched =
360
407
  await this.#fetch${pascalCase(property.singularName)}(v, options);
361
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;
362
412
  this.${await getFieldName(property.uri)}[0] = fetched;
363
413
  this.${await getFieldName(property.uri, "#_trust")}.add(0);
364
414
  this._cachedJsonLd = undefined;
@@ -398,7 +448,9 @@ async function* generateProperty(
398
448
  type.trustEmbeddedObjects === false ? "" : "this.id != null && "
399
449
  }!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
400
450
  isUnsecuredPortableObject(this) ||
401
- (isInPortableChain(this) && isCompatibleEf61Iri(v.id))) &&
451
+ mustDereferencePortableObject(
452
+ this, ${trustOwner}, v.id, options,
453
+ )) &&
402
454
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
403
455
  if (options.crossOrigin === "throw") {
404
456
  throw new Error(
@@ -485,7 +537,7 @@ async function* generateProperty(
485
537
  } else if (
486
538
  !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
487
539
  isUnsecuredPortableObject(this) ||
488
- (isInPortableChain(this) && isCompatibleEf61Iri(v.id))
540
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
489
541
  ) {
490
542
  v = v.id;
491
543
  }
@@ -494,6 +546,12 @@ async function* generateProperty(
494
546
  const fetched =
495
547
  await this.#fetch${pascalCase(property.singularName)}(v, options);
496
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
+ }
497
555
  vs[i] = fetched;
498
556
  this.${await getFieldName(property.uri, "#_trust")}.add(i);
499
557
  this._cachedJsonLd = undefined;
@@ -534,7 +592,9 @@ async function* generateProperty(
534
592
  type.trustEmbeddedObjects === false ? "" : "this.id != null && "
535
593
  }!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
536
594
  isUnsecuredPortableObject(this) ||
537
- (isInPortableChain(this) && isCompatibleEf61Iri(v.id))) &&
595
+ mustDereferencePortableObject(
596
+ this, ${trustOwner}, v.id, options,
597
+ )) &&
538
598
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
539
599
  if (options.crossOrigin === "throw") {
540
600
  throw new Error(