@fedify/vocab-tools 2.4.0-pr.936.41 → 2.5.0-dev.2271

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/dist/mod.js CHANGED
@@ -7,7 +7,7 @@ import { parse } from "yaml";
7
7
  import { pascalCase } from "es-toolkit";
8
8
  //#region deno.json
9
9
  var name = "@fedify/vocab-tools";
10
- var version = "2.4.0-pr.936.41+59c7432c";
10
+ var version = "2.5.0-dev.2271+49d937e2";
11
11
  //#endregion
12
12
  //#region src/type.ts
13
13
  const HEURISTICS_CONTEXTS = [
@@ -185,7 +185,8 @@ const scalarTypes = {
185
185
  dataCheck(v) {
186
186
  return `typeof ${v} === "object" && "@language" in ${v} && "@value" in ${v}
187
187
  && typeof ${v}["@language"] === "string"
188
- && typeof ${v}["@value"] === "string"`;
188
+ && typeof ${v}["@value"] === "string"
189
+ && isValidLanguageTag(${v}["@language"])`;
189
190
  },
190
191
  decoder(v) {
191
192
  return `new LanguageString(${v}["@value"], ${v}["@language"])`;
@@ -316,13 +317,14 @@ const scalarTypes = {
316
317
  return `formatIri(${v})`;
317
318
  },
318
319
  dataCheck(v) {
319
- return `typeof ${v} === "object" && "@value" in ${v}
320
- && typeof ${v}["@value"] === "string"
321
- && ${v}["@value"] !== "" && ${v}["@value"] !== "/"`;
320
+ return `typeof ${v} === "object" &&
321
+ ((typeof ${v}["@id"] === "string" && canDecodeIri(${v}["@id"])) ||
322
+ (typeof ${v}["@value"] === "string" && canDecodeIri(${v}["@value"])))`;
322
323
  },
323
324
  decoder(v) {
324
- return `parseIri(${v}["@value"])`;
325
- }
325
+ return `decodeIri(typeof ${v}["@id"] === "string" ? ${v}["@id"] : ${v}["@value"])`;
326
+ },
327
+ skipUnparsable: true
326
328
  },
327
329
  "fedify:gatewayUrl": {
328
330
  name: "URL",
@@ -330,10 +332,10 @@ const scalarTypes = {
330
332
  return `${v} instanceof URL && isGatewayUrl(${v})`;
331
333
  },
332
334
  encoder(v) {
333
- return `{ "@id": formatIri(${v}) }`;
335
+ return `{ "@id": ${v}.origin }`;
334
336
  },
335
337
  compactEncoder(v) {
336
- return `formatIri(${v})`;
338
+ return `${v}.origin`;
337
339
  },
338
340
  dataCheck(v) {
339
341
  return `${v} != null && typeof ${v} === "object" &&
@@ -556,6 +558,9 @@ function getDecoder(typeUri, types, variable, optionsVariable, baseUrlVariable)
556
558
  )`;
557
559
  throw new Error(`Unknown type: ${typeUri}`);
558
560
  }
561
+ function skipsUnparsable(typeUri) {
562
+ return scalarTypes[typeUri]?.skipUnparsable ?? false;
563
+ }
559
564
  function getDataCheck(typeUri, types, variable) {
560
565
  if (typeUri in scalarTypes) return scalarTypes[typeUri].dataCheck(variable);
561
566
  if (typeUri in types) {
@@ -735,6 +740,7 @@ function* generatePreprocessorBlock(property, rangeTypeName, variable, baseUrlEx
735
740
  documentLoader: options.documentLoader,
736
741
  contextLoader: options.contextLoader,
737
742
  tracerProvider: options.tracerProvider,
743
+ verifyPortableObject: options.verifyPortableObject,
738
744
  baseUrl: ${baseUrlExpr},
739
745
  });
740
746
  if (_result instanceof Error) throw _result;
@@ -783,6 +789,10 @@ async function* generateEncoder(typeUri, types) {
783
789
  ...options,
784
790
  contextLoader: options.contextLoader ?? getDocumentLoader(),
785
791
  };
792
+ // Joins the enclosing frame's signed-value scope, or starts one when this
793
+ // frame is the outermost. Only the owning frame puts retained signed
794
+ // child documents back in place of their placeholders.
795
+ const signedValues = enterSignedValueScope(options);
786
796
  `;
787
797
  if (isCompactableType(typeUri, types)) {
788
798
  yield `
@@ -813,9 +823,13 @@ async function* generateEncoder(typeUri, types) {
813
823
  for (const v of this.${await getFieldName(property.uri)}) {
814
824
  const item = (
815
825
  `;
816
- if (!areAllScalarTypes(property.range, types)) yield "v instanceof URL ? formatIri(v) : ";
826
+ if (!areAllScalarTypes(property.range, types)) {
827
+ yield "retainedSignedValueRef(v, signedValues.scope) ?? (";
828
+ yield "v instanceof URL ? formatIri(v) : ";
829
+ }
817
830
  const encoders = getEncoders(property.range, types, "v", "options", true);
818
831
  for (const code of encoders) yield code;
832
+ if (!areAllScalarTypes(property.range, types)) yield ")";
819
833
  yield `
820
834
  );
821
835
  compactItems.push(item);
@@ -846,7 +860,9 @@ async function* generateEncoder(typeUri, types) {
846
860
  ${type.typeless ? "" : `result["type"] = ${JSON.stringify(type.compactName ?? type.uri)};`}
847
861
  if (this.id != null) result["id"] = formatIri(this.id);
848
862
  result["@context"] = ${JSON.stringify(type.defaultContext)};
849
- return result;
863
+ return signedValues.owner
864
+ ? resolveSignedValues(result, signedValues.scope)
865
+ : result;
850
866
  }
851
867
  `;
852
868
  }
@@ -870,12 +886,21 @@ async function* generateEncoder(typeUri, types) {
870
886
  yield `
871
887
  array = [];
872
888
  for (const v of this.${await getFieldName(property.uri)}) {
873
- const element = (
889
+ let element = (
874
890
  `;
875
- if (!areAllScalarTypes(property.range, types)) yield "v instanceof URL ? { \"@id\": formatIri(v) } : ";
891
+ if (!areAllScalarTypes(property.range, types)) {
892
+ yield "retainedSignedValueRef(v, signedValues.scope) ?? (";
893
+ yield "v instanceof URL ? { \"@id\": formatIri(v) } : ";
894
+ }
876
895
  for (const code of getEncoders(property.range, types, "v", "options")) yield code;
896
+ if (!areAllScalarTypes(property.range, types)) yield ")";
877
897
  yield `
878
898
  );
899
+ if (Array.isArray(element)) {
900
+ if (element.length < 1) continue;
901
+ if (element.length == 1) element = element[0];
902
+ }
903
+ if (typeof element === "undefined") continue;
879
904
  `;
880
905
  if (isNonFunctionalProperty(property) && property.container === "graph") yield `array.push({ "@graph": element });`;
881
906
  else yield `array.push(element);`;
@@ -903,16 +928,35 @@ async function* generateEncoder(typeUri, types) {
903
928
  if (options.format === "expand") {
904
929
  return await jsonld.expand(
905
930
  values,
906
- { documentLoader: options.contextLoader },
931
+ {
932
+ documentLoader: options.contextLoader,
933
+ keepFreeFloatingNodes: true,
934
+ },
907
935
  );
908
936
  }
909
- const docContext = options.context ??
937
+ let docContext: Parameters<typeof jsonld.compact>[1] = options.context ??
910
938
  ${JSON.stringify(type.defaultContext)};
911
- const compacted = await jsonld.compact(
939
+ let compacted = await jsonld.compact(
912
940
  values,
913
941
  docContext,
914
942
  { documentLoader: options.contextLoader },
915
943
  );
944
+ `;
945
+ if (Object.values(types).some((t) => t.properties.some((p) => p.extraContext != null))) yield `
946
+ if (options.context == null) {
947
+ const currentContexts = Array.isArray(docContext) ? docContext : [docContext];
948
+ const additionalContexts = getExtraContexts(compacted).filter(
949
+ context => !currentContexts.includes(context)
950
+ );
951
+ if (additionalContexts.length > 0) {
952
+ docContext = [...currentContexts, ...additionalContexts];
953
+ compacted = await jsonld.compact(values, docContext, {
954
+ documentLoader: options.contextLoader,
955
+ });
956
+ }
957
+ }
958
+ `;
959
+ yield `
916
960
  if (docContext != null) {
917
961
  // Embed context
918
962
  `;
@@ -936,12 +980,14 @@ async function* generateEncoder(typeUri, types) {
936
980
  }
937
981
  yield `
938
982
  }
939
- return compacted;
983
+ return signedValues.owner
984
+ ? resolveSignedValues(compacted, signedValues.scope)
985
+ : compacted;
940
986
  }
941
987
 
942
988
  protected ${emitOverride$1(typeUri, types)} isCompactable(): boolean {
943
989
  `;
944
- for (const property of type.properties) if (!property.range.every((r) => isCompactableType(r, types))) yield `
990
+ for (const property of type.properties) if (property.extraContext != null || !property.range.every((r) => isCompactableType(r, types))) yield `
945
991
  if (
946
992
  this.${await getFieldName(property.uri)} != null &&
947
993
  this.${await getFieldName(property.uri)}.length > 0
@@ -963,6 +1009,10 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
963
1009
  * - \`contextLoader\`: The loader for remote JSON-LD contexts.
964
1010
  * - \`tracerProvider\`: The OpenTelemetry tracer provider to use.
965
1011
  * If omitted, the global tracer provider is used.
1012
+ * - \`verifyPortableObject\`: The default FEP-ef61 portable
1013
+ * object verifier for the property accessors of the
1014
+ * returned object and the objects obtained from it. It
1015
+ * does not verify the given \`json\` itself.
966
1016
  * @returns The object of this type.
967
1017
  * @throws {TypeError} If the given \`json\` is invalid.
968
1018
  */
@@ -972,6 +1022,7 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
972
1022
  documentLoader?: DocumentLoader,
973
1023
  contextLoader?: DocumentLoader,
974
1024
  tracerProvider?: TracerProvider,
1025
+ verifyPortableObject?: PortableObjectVerifier,
975
1026
  baseUrl?: URL,
976
1027
  } = {},
977
1028
  ): Promise<${type.name}> {
@@ -1010,6 +1061,7 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1010
1061
  documentLoader?: DocumentLoader,
1011
1062
  contextLoader?: DocumentLoader,
1012
1063
  tracerProvider?: TracerProvider,
1064
+ verifyPortableObject?: PortableObjectVerifier,
1013
1065
  baseUrl?: URL,
1014
1066
  } = {},
1015
1067
  ): Promise<${type.name}> {
@@ -1026,7 +1078,17 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1026
1078
  // deno-lint-ignore no-explicit-any
1027
1079
  let values: Record<string, any[]> & { "@id"?: string };
1028
1080
  let expanded: unknown[];
1029
- if (globalThis.Object.keys(json).length == 0) {
1081
+ if (
1082
+ "_fromSubclass" in options &&
1083
+ options._fromSubclass &&
1084
+ typeof json === "object" &&
1085
+ !Array.isArray(json)
1086
+ ) {
1087
+ values =
1088
+ // deno-lint-ignore no-explicit-any
1089
+ json as (Record<string, any[]> & { "@id"?: string });
1090
+ expanded = [values];
1091
+ } else if (globalThis.Object.keys(json).length == 0) {
1030
1092
  values = {};
1031
1093
  expanded = [values];
1032
1094
  } else {
@@ -1083,8 +1145,9 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1083
1145
  );
1084
1146
  `;
1085
1147
  else yield `
1086
- delete values["@type"];
1087
- const instance = await super.fromJsonLd(values, {
1148
+ const superValues = structuredClone(values);
1149
+ delete superValues["@type"];
1150
+ const instance = await super.fromJsonLd(superValues, {
1088
1151
  ...options,
1089
1152
  // @ts-ignore: an internal option
1090
1153
  _fromSubclass: true,
@@ -1093,25 +1156,35 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1093
1156
  throw new TypeError("Unexpected type: " + instance.constructor.name);
1094
1157
  }
1095
1158
  `;
1159
+ yield `
1160
+ let shouldCacheJsonLd = instance._shouldCacheJsonLd;
1161
+ `;
1096
1162
  for (const property of type.properties) {
1097
1163
  const variable = await getFieldName(property.uri, "");
1098
1164
  yield await generateField(property, types, "const ");
1099
1165
  const arrayVariable = `${variable}__array`;
1100
1166
  yield `
1101
- let ${arrayVariable} = values[${JSON.stringify(property.uri)}];
1167
+ let ${arrayVariable} = ${property.uri === "https://w3id.org/fep/ef61/gateways" ? "getPortableActorGateways(values) as typeof values[string]" : `values[${JSON.stringify(property.uri)}]`};
1102
1168
  `;
1103
1169
  if (property.functional && property.redundantProperties != null) for (const prop of property.redundantProperties) yield `
1104
1170
  if (${arrayVariable} == null || ${arrayVariable}.length < 1) {
1105
1171
  ${arrayVariable} = values[${JSON.stringify(prop.uri)}];
1106
1172
  }
1107
1173
  `;
1174
+ let itemsExpression = `${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1175
+ ? ${arrayVariable}[0]["@list"]
1176
+ : ${arrayVariable}`;
1177
+ if (isNonFunctionalProperty(property) && property.container === "graph") itemsExpression = `(${itemsExpression}).flatMap((item: unknown) =>
1178
+ item != null && typeof item === "object" && "@graph" in item &&
1179
+ Array.isArray((item as { "@graph": unknown })["@graph"])
1180
+ ? (item as { "@graph": unknown[] })["@graph"]
1181
+ : [item]
1182
+ )`;
1108
1183
  yield `
1109
1184
  for (
1110
1185
  const v of ${arrayVariable} == null
1111
1186
  ? []
1112
- : ${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1113
- ? ${arrayVariable}[0]["@list"]
1114
- : ${arrayVariable}
1187
+ : ${itemsExpression}
1115
1188
  ) {
1116
1189
  if (v == null) continue;
1117
1190
  `;
@@ -1125,29 +1198,47 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1125
1198
  continue;
1126
1199
  }
1127
1200
  `;
1128
- if (property.range.length == 1) yield `
1129
- const decoded = ${getDecoder(property.range[0], types, "v", "options", propertyBaseUrl)};
1130
- if (typeof decoded === "undefined") continue;
1131
- ${variable}.push(decoded);`;
1132
- else {
1133
- yield `
1201
+ yield `
1134
1202
  const decoded =
1135
- `;
1203
+ `;
1204
+ const lenient = property.range.length == 1 && skipsUnparsable(property.range[0]);
1205
+ if (property.range.length == 1) {
1206
+ if (lenient) {
1207
+ yield getDataCheck(property.range[0], types, "v");
1208
+ yield " ? ";
1209
+ }
1210
+ yield getDecoder(property.range[0], types, "v", "options", propertyBaseUrl);
1211
+ if (lenient) yield " : undefined";
1212
+ } else {
1136
1213
  const decoders = getDecoders(property.range, types, "v", "options", propertyBaseUrl);
1137
1214
  for (const code of decoders) yield code;
1138
- yield `
1215
+ }
1216
+ yield `
1139
1217
  ;
1140
- if (typeof decoded === "undefined") continue;
1218
+ `;
1219
+ yield `
1220
+ if (typeof decoded === "undefined") {
1221
+ shouldCacheJsonLd = false;
1222
+ continue;
1223
+ }
1224
+ `;
1225
+ yield `
1226
+ if (!this._shouldCacheDecodedJsonLd(decoded)) {
1227
+ shouldCacheJsonLd = false;
1228
+ }
1141
1229
  ${variable}.push(decoded);
1142
- `;
1143
- }
1230
+ `;
1144
1231
  yield `
1145
1232
  }
1146
1233
  instance.${await getFieldName(property.uri)} = ${variable};
1147
1234
  `;
1148
1235
  }
1149
1236
  yield `
1150
- if (!("_fromSubclass" in options) || !options._fromSubclass) {
1237
+ instance._shouldCacheJsonLd = shouldCacheJsonLd;
1238
+ if (
1239
+ shouldCacheJsonLd &&
1240
+ (!("_fromSubclass" in options) || !options._fromSubclass)
1241
+ ) {
1151
1242
  try {
1152
1243
  if (cacheJsonLd != null && cacheJsonLd !== expanded) {
1153
1244
  const compactArray = Array.isArray(json) && json.length === 1;
@@ -1213,6 +1304,7 @@ async function* generateConstructor(typeUri, types) {
1213
1304
  documentLoader?: DocumentLoader,
1214
1305
  contextLoader?: DocumentLoader,
1215
1306
  tracerProvider?: TracerProvider,
1307
+ verifyPortableObject?: PortableObjectVerifier,
1216
1308
  } = {},
1217
1309
  ) {
1218
1310
  `;
@@ -1220,6 +1312,7 @@ async function* generateConstructor(typeUri, types) {
1220
1312
  this.#documentLoader = options.documentLoader;
1221
1313
  this.#contextLoader = options.contextLoader;
1222
1314
  this.#tracerProvider = options.tracerProvider;
1315
+ this.#verifyPortableObject = options.verifyPortableObject;
1223
1316
  const baseUrl = (options as { baseUrl?: URL }).baseUrl;
1224
1317
  this.#_baseUrl = baseUrl == null ? undefined : new URL(baseUrl.href);
1225
1318
  if ("$warning" in options) {
@@ -1314,6 +1407,13 @@ async function* generateCloner(typeUri, types) {
1314
1407
  yield `
1315
1408
  /**
1316
1409
  * Clones this instance, optionally updating it with the given values.
1410
+ *
1411
+ * A clone never inherits a signed JSON-LD representation retained by
1412
+ * \`signObject()\`: the clone may differ from the document that the proof
1413
+ * covers, so embedding the original secured JSON in a parent document
1414
+ * would be wrong. Sign the clone again if it has to be embedded as a
1415
+ * secured child.
1416
+ *
1317
1417
  * @param values The values to update the clone with.
1318
1418
  * @param options The options to use for cloning.
1319
1419
  * @returns The cloned instance.
@@ -1327,6 +1427,7 @@ async function* generateCloner(typeUri, types) {
1327
1427
  options: {
1328
1428
  documentLoader?: DocumentLoader,
1329
1429
  contextLoader?: DocumentLoader,
1430
+ verifyPortableObject?: PortableObjectVerifier,
1330
1431
  } = {}
1331
1432
  ): ${type.name} {
1332
1433
  if (this._warning != null) {
@@ -1344,6 +1445,7 @@ async function* generateCloner(typeUri, types) {
1344
1445
  { id: values.id ?? this.id },
1345
1446
  options
1346
1447
  );
1448
+ copyPortableProvenance(this, clone);
1347
1449
  `;
1348
1450
  else yield `const clone = super.clone(values, options) as unknown as ${type.name};`;
1349
1451
  for (const property of type.properties) {
@@ -1439,12 +1541,12 @@ async function* generateInspector(typeUri, types) {
1439
1541
  [Symbol.for("Deno.customInspect")]: (
1440
1542
  inspect: typeof Deno.inspect,
1441
1543
  options: Deno.InspectOptions,
1442
- ): string => "URL " + inspect(this.id!.href, options),
1544
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
1443
1545
  [Symbol.for("nodejs.util.inspect.custom")]: (
1444
1546
  _depth: number,
1445
1547
  options: unknown,
1446
1548
  inspect: (value: unknown, options: unknown) => string,
1447
- ): string => "URL " + inspect(this.id!.href, options),
1549
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
1448
1550
  };
1449
1551
  }
1450
1552
  `;
@@ -1460,12 +1562,12 @@ async function* generateInspector(typeUri, types) {
1460
1562
  [Symbol.for("Deno.customInspect")]: (
1461
1563
  inspect: typeof Deno.inspect,
1462
1564
  options: Deno.InspectOptions,
1463
- ): string => "URL " + inspect(v.href, options),
1565
+ ): string => "URL " + inspect(inspectIri(v), options),
1464
1566
  [Symbol.for("nodejs.util.inspect.custom")]: (
1465
1567
  _depth: number,
1466
1568
  options: unknown,
1467
1569
  inspect: (value: unknown, options: unknown) => string,
1468
- ): string => "URL " + inspect(v.href, options),
1570
+ ): string => "URL " + inspect(inspectIri(v), options),
1469
1571
  }
1470
1572
  : v);
1471
1573
  `;
@@ -1535,6 +1637,7 @@ function emitOverride(typeUri, types, property) {
1535
1637
  }
1536
1638
  async function* generateProperty(type, property, types, moduleVarNames) {
1537
1639
  const override = emitOverride(type.uri, types, property);
1640
+ const trustOwner = type.trustEmbeddedObjects === false ? "null" : "this.id";
1538
1641
  const doc = `\n/** ${property.description.replaceAll("\n", "\n * ")}\n */\n`;
1539
1642
  const cachedPropertyBaseUrl = `(options as { baseUrl?: URL }).baseUrl ??
1540
1643
  this._baseUrl ??
@@ -1575,6 +1678,9 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1575
1678
  suppressError?: boolean,
1576
1679
  tracerProvider?: TracerProvider,
1577
1680
  crossOrigin?: "ignore" | "throw" | "trust";
1681
+ gateways?: readonly (string | URL)[];
1682
+ verifyPortableObject?: PortableObjectVerifier;
1683
+ inheritPortableObjectVerifier?: boolean;
1578
1684
  } = {},
1579
1685
  ): Promise<${getTypeNames(property.range, types)} | null> {
1580
1686
  const documentLoader =
@@ -1587,11 +1693,96 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1587
1693
  ${JSON.stringify(name)},
1588
1694
  ${JSON.stringify(version)},
1589
1695
  );
1696
+ // The fetched object uses the verifier given to this call (which falls
1697
+ // back to the parent's own default) for its accessors by default,
1698
+ // unless the call keeps its verifier to itself:
1699
+ const childVerifier = options.inheritPortableObjectVerifier === false
1700
+ ? this._verifyPortableObject
1701
+ : options.verifyPortableObject;
1590
1702
  return await tracer.startActiveSpan("activitypub.lookup_object", async (span) => {
1703
+ const dereferencePortable = async (
1704
+ portableUrl: URL,
1705
+ extra: {
1706
+ inferredGateways?: readonly URL[];
1707
+ response?: RemoteDocument;
1708
+ contextLoader?: DocumentLoader;
1709
+ } = {},
1710
+ ) => {
1711
+ try {
1712
+ const obj = await dereferencePortableIri(portableUrl, {
1713
+ documentLoader,
1714
+ contextLoader: extra.contextLoader ?? contextLoader,
1715
+ tracerProvider,
1716
+ gateways: options.gateways,
1717
+ inferredGateways: extra.inferredGateways,
1718
+ response: extra.response,
1719
+ verifyPortableObject: options.verifyPortableObject,
1720
+ referrer: {
1721
+ object: this,
1722
+ property: ${JSON.stringify(property.uri)},
1723
+ },
1724
+ suppressError: options.suppressError,
1725
+ crossOrigin: options.crossOrigin,
1726
+ span,
1727
+ parse: (document, parseOptions) =>
1728
+ this.#${property.singularName}_fromJsonLd(document, {
1729
+ documentLoader,
1730
+ contextLoader: parseOptions.contextLoader,
1731
+ tracerProvider,
1732
+ verifyPortableObject: childVerifier,
1733
+ baseUrl: parseOptions.baseUrl,
1734
+ }),
1735
+ });
1736
+ if (obj != null) {
1737
+ span.setAttribute(
1738
+ "activitypub.object.id",
1739
+ (obj.id ?? portableUrl).href,
1740
+ );
1741
+ span.setAttribute(
1742
+ "activitypub.object.type",
1743
+ // @ts-ignore: obj.constructor always has a typeId.
1744
+ obj.constructor.typeId.href
1745
+ );
1746
+ }
1747
+ return obj;
1748
+ } catch (e) {
1749
+ span.setStatus({
1750
+ code: SpanStatusCode.ERROR,
1751
+ message: String(e),
1752
+ });
1753
+ throw e;
1754
+ } finally {
1755
+ span.end();
1756
+ }
1757
+ };
1758
+ if (isPortableIri(url)) {
1759
+ // A reference in a portable actor's own document, e.g., its outbox,
1760
+ // has no location hints, so the actor's gateways are used instead:
1761
+ return await dereferencePortable(
1762
+ url,
1763
+ options.gateways == null
1764
+ ? { inferredGateways: getReferrerGateways(this, url) }
1765
+ : {},
1766
+ );
1767
+ }
1591
1768
  const lookupUrl = formatIri(url);
1769
+ const portableMode = isPortableMode(this, options);
1770
+ if (portableMode) {
1771
+ const compatible = parseCompatibleEf61Reference(url);
1772
+ if (compatible === null) {
1773
+ span.end();
1774
+ return rejectMalformedCompatibleReference(lookupUrl, options);
1775
+ } else if (compatible != null) {
1776
+ return await dereferencePortable(compatible.id, {
1777
+ inferredGateways: [compatible.gateway],
1778
+ });
1779
+ }
1780
+ }
1592
1781
  let fetchResult: RemoteDocument;
1593
1782
  try {
1594
- fetchResult = await documentLoader(lookupUrl);
1783
+ fetchResult = await documentLoader(lookupUrl, {
1784
+ suppressError: options.suppressError,
1785
+ });
1595
1786
  } catch (error) {
1596
1787
  span.setStatus({
1597
1788
  code: SpanStatusCode.ERROR,
@@ -1599,7 +1790,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1599
1790
  });
1600
1791
  span.end();
1601
1792
  if (options.suppressError) {
1602
- getLogger(["fedify", "vocab"]).error(
1793
+ getLogger(["fedify", "vocab"]).warn(
1603
1794
  "Failed to fetch {url}: {error}",
1604
1795
  { error, url: lookupUrl }
1605
1796
  );
@@ -1608,52 +1799,96 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1608
1799
  throw error;
1609
1800
  }
1610
1801
  const { document, documentUrl } = fetchResult;
1611
- const baseUrl = parseIri(documentUrl);
1802
+ // In portable mode, the document may turn out to be a portable object,
1803
+ // which has to be verified with the same context documents:
1804
+ const snapshot = portableMode
1805
+ ? createSnapshotContextLoader(contextLoader, options.suppressError)
1806
+ : null;
1807
+ const scopedContext = createScopedContextLoader(
1808
+ snapshot?.loader ?? contextLoader,
1809
+ snapshot == null && options.suppressError,
1810
+ );
1612
1811
  try {
1613
- const obj = await this.#${property.singularName}_fromJsonLd(
1614
- document,
1615
- { documentLoader, contextLoader, tracerProvider, baseUrl }
1616
- );
1617
- if (obj?.id != null && !isTrustedIriOrigin(options, obj.id, baseUrl)) {
1618
- if (options.crossOrigin === "throw") {
1619
- throw new Error(
1620
- "The object's @id (" + obj.id.href + ") has a different origin " +
1621
- "than the document URL (" + baseUrl.href + "); refusing to return " +
1622
- "the object. If you want to bypass this check and are aware of" +
1623
- 'the security implications, set the crossOrigin option to "trust".'
1812
+ let claim: PortableResponseClaim | null | undefined;
1813
+ try {
1814
+ const baseUrl = parseIri(documentUrl);
1815
+ const obj = await this.#${property.singularName}_fromJsonLd(
1816
+ document,
1817
+ {
1818
+ documentLoader,
1819
+ contextLoader: scopedContext.loader,
1820
+ tracerProvider,
1821
+ verifyPortableObject: childVerifier,
1822
+ baseUrl,
1823
+ }
1824
+ );
1825
+ if (snapshot == null) {
1826
+ markUnverifiedPortableClaim(obj, url, documentUrl);
1827
+ } else {
1828
+ claim = getPortableResponseClaim(documentUrl, obj.id);
1829
+ }
1830
+ if (claim === undefined) {
1831
+ if (
1832
+ obj?.id != null &&
1833
+ !isTrustedIriOrigin(options, obj.id, baseUrl)
1834
+ ) {
1835
+ if (options.crossOrigin === "throw") {
1836
+ throw new Error(
1837
+ "The object's @id (" + obj.id.href + ") has a different origin " +
1838
+ "than the document URL (" + baseUrl.href + "); refusing to return " +
1839
+ "the object. If you want to bypass this check and are aware of" +
1840
+ 'the security implications, set the crossOrigin option to "trust".'
1841
+ );
1842
+ }
1843
+ getLogger(["fedify", "vocab"]).warn(
1844
+ "The object's @id ({objectId}) has a different origin than the document " +
1845
+ "URL ({documentUrl}); refusing to return the object. If you want to " +
1846
+ "bypass this check and are aware of the security implications, " +
1847
+ 'set the crossOrigin option to "trust".',
1848
+ { ...fetchResult, objectId: obj.id.href },
1849
+ );
1850
+ span.end();
1851
+ return null;
1852
+ }
1853
+ span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
1854
+ span.setAttribute(
1855
+ "activitypub.object.type",
1856
+ // @ts-ignore: obj.constructor always has a typeId.
1857
+ obj.constructor.typeId.href
1624
1858
  );
1859
+ span.end();
1860
+ return obj;
1625
1861
  }
1626
- getLogger(["fedify", "vocab"]).warn(
1627
- "The object's @id ({objectId}) has a different origin than the document " +
1628
- "URL ({documentUrl}); refusing to return the object. If you want to " +
1629
- "bypass this check and are aware of the security implications, " +
1630
- 'set the crossOrigin option to "trust".',
1631
- { ...fetchResult, objectId: obj.id.href },
1632
- );
1633
- return null;
1862
+ } catch (e) {
1863
+ if (options.suppressError) {
1864
+ getLogger(["fedify", "vocab"]).warn(
1865
+ "Failed to parse {url}: {error}",
1866
+ { error: e, url: lookupUrl }
1867
+ );
1868
+ span.end();
1869
+ return null;
1870
+ }
1871
+ span.setStatus({
1872
+ code: SpanStatusCode.ERROR,
1873
+ message: String(e),
1874
+ });
1875
+ span.end();
1876
+ throw e;
1634
1877
  }
1635
- span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
1636
- span.setAttribute(
1637
- "activitypub.object.type",
1638
- // @ts-ignore: obj.constructor always has a typeId.
1639
- obj.constructor.typeId.href
1640
- );
1641
- return obj;
1642
- } catch (e) {
1643
- if (options.suppressError) {
1644
- getLogger(["fedify", "vocab"]).error(
1645
- "Failed to parse {url}: {error}",
1646
- { error: e, url: lookupUrl }
1647
- );
1648
- return null;
1878
+ // The document stands for a portable object, so it is verified as
1879
+ // one instead of being trusted because of where it came from:
1880
+ if (claim === null) {
1881
+ span.end();
1882
+ return rejectMalformedCompatibleReference(documentUrl, options);
1649
1883
  }
1650
- span.setStatus({
1651
- code: SpanStatusCode.ERROR,
1652
- message: String(e),
1884
+ return await dereferencePortable(claim.id, {
1885
+ inferredGateways: claim.inferredGateways,
1886
+ response: fetchResult,
1887
+ contextLoader: snapshot?.loader,
1653
1888
  });
1654
- throw e;
1655
1889
  } finally {
1656
- span.end();
1890
+ scopedContext.release();
1891
+ snapshot?.release();
1657
1892
  }
1658
1893
  });
1659
1894
  }
@@ -1664,6 +1899,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1664
1899
  documentLoader?: DocumentLoader,
1665
1900
  contextLoader?: DocumentLoader,
1666
1901
  tracerProvider?: TracerProvider,
1902
+ verifyPortableObject?: PortableObjectVerifier,
1667
1903
  baseUrl?: URL
1668
1904
  }
1669
1905
  ): Promise<${getTypeNames(property.range, types)}> {
@@ -1674,6 +1910,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1674
1910
  const tracerProvider = options.tracerProvider ??
1675
1911
  this._tracerProvider ?? trace.getTracerProvider();
1676
1912
  const baseUrl = options.baseUrl;
1913
+ const verifyPortableObject = options.verifyPortableObject;
1677
1914
  `;
1678
1915
  if (property.preprocessors != null && property.preprocessors.length > 0) {
1679
1916
  yield `
@@ -1693,6 +1930,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1693
1930
  documentLoader,
1694
1931
  contextLoader,
1695
1932
  tracerProvider,
1933
+ verifyPortableObject,
1696
1934
  baseUrl,
1697
1935
  });
1698
1936
  if (_result instanceof Error) throw _result;
@@ -1711,7 +1949,13 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1711
1949
  try {
1712
1950
  return await ${types[range].name}.fromJsonLd(
1713
1951
  jsonLd,
1714
- { documentLoader, contextLoader, tracerProvider, baseUrl },
1952
+ {
1953
+ documentLoader,
1954
+ contextLoader,
1955
+ tracerProvider,
1956
+ verifyPortableObject,
1957
+ baseUrl,
1958
+ },
1715
1959
  );
1716
1960
  } catch (e) {
1717
1961
  if (!(e instanceof TypeError)) throw e;
@@ -1753,6 +1997,21 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1753
1997
  suppressError?: boolean,
1754
1998
  tracerProvider?: TracerProvider,
1755
1999
  crossOrigin?: "ignore" | "throw" | "trust";
2000
+ gateways?: readonly (string | URL)[];
2001
+ verifyPortableObject?: PortableObjectVerifier;
2002
+ /**
2003
+ * Whether objects that this call newly fetches use the
2004
+ * \`verifyPortableObject\` option of this call as their default
2005
+ * verifier. Defaults to \`true\`. If \`false\`, the option
2006
+ * applies to this call only, and fetched objects use this object's
2007
+ * own default verifier, if any, as their default instead. Pass
2008
+ * \`false\` for a temporary verifier, e.g., one that accepts
2009
+ * everything. Objects that were already cached in this object or
2010
+ * embedded in it keep their defaults either way, and this option
2011
+ * itself is not passed on.
2012
+ * @since 2.4.0
2013
+ */
2014
+ inheritPortableObjectVerifier?: boolean;
1756
2015
  } = {}
1757
2016
  ): Promise<${getTypeNames(property.range, types)} | null> {
1758
2017
  if (this._warning != null) {
@@ -1762,25 +2021,46 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1762
2021
  );
1763
2022
  }
1764
2023
  if (this.${await getFieldName(property.uri)}.length < 1) return null;
2024
+ if (
2025
+ options.verifyPortableObject == null &&
2026
+ this._verifyPortableObject != null
2027
+ ) {
2028
+ options = {
2029
+ ...options,
2030
+ verifyPortableObject: this._verifyPortableObject,
2031
+ };
2032
+ }
1765
2033
  let v = this.${await getFieldName(property.uri)}[0];
1766
2034
  if (!(v instanceof URL) &&
1767
- v.id != null &&
1768
- !isTrustedIriOrigin(options, v.id, this.id) &&
1769
2035
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
1770
- v = v.id;
2036
+ if (v.id == null) {
2037
+ if (isUnsecuredPortableObject(this)) {
2038
+ warnUnverifiableEmbeddedObject(this, ${JSON.stringify(property.uri)});
2039
+ return null;
2040
+ }
2041
+ } else if (
2042
+ !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
2043
+ isUnsecuredPortableObject(this) ||
2044
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
2045
+ ) {
2046
+ v = v.id;
2047
+ }
1771
2048
  }
1772
2049
  if (v instanceof URL) {
1773
2050
  const fetched =
1774
2051
  await this.#fetch${pascalCase(property.singularName)}(v, options);
1775
2052
  if (fetched == null) return null;
2053
+ // Not cached, so that it is verified when the portable object
2054
+ // policy applies later:
2055
+ if (isUnverifiedPortableClaim(fetched)) return fetched;
1776
2056
  this.${await getFieldName(property.uri)}[0] = fetched;
1777
2057
  this.${await getFieldName(property.uri, "#_trust")}.add(0);
1778
2058
  this._cachedJsonLd = undefined;
2059
+ recordPortableReferrer(this, fetched, ${JSON.stringify(property.uri)});
1779
2060
  return fetched;
1780
2061
  }
1781
2062
  `;
1782
- if (property.compactName != null) {
1783
- yield `
2063
+ if (property.compactName != null) yield `
1784
2064
  if (
1785
2065
  this._cachedJsonLd != null &&
1786
2066
  typeof this._cachedJsonLd === "object" &&
@@ -1791,26 +2071,36 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1791
2071
  ${JSON.stringify(property.compactName)}];
1792
2072
  const doc = Array.isArray(prop) ? prop[0] : prop;
1793
2073
  if (doc != null && typeof doc === "object" && "@context" in doc) {
1794
- `;
1795
- yield `
1796
- v = await this.#${property.singularName}_fromJsonLd(doc, {
1797
- ...options,
1798
- baseUrl: ${cachedPropertyBaseUrl},
1799
- });
1800
- `;
1801
- yield `
2074
+ try {
2075
+ v = await this.#${property.singularName}_fromJsonLd(doc, {
2076
+ ...options,
2077
+ // Keep the parent default for embedded objects.
2078
+ verifyPortableObject: this._verifyPortableObject,
2079
+ baseUrl: ${cachedPropertyBaseUrl},
2080
+ });
2081
+ } catch (error) {
2082
+ if (!options.suppressError) throw error;
2083
+ getLogger(["fedify", "vocab"]).debug(
2084
+ "Failed to parse embedded value of {property}: {error}",
2085
+ { property: ${JSON.stringify(property.uri)}, error },
2086
+ );
2087
+ return null;
2088
+ }
1802
2089
  }
1803
2090
  }
1804
2091
  `;
1805
- }
1806
2092
  yield `
1807
2093
  if (v?.id != null &&
1808
- this.id != null && !isTrustedIriOrigin(options, v.id, this.id) &&
2094
+ ((${type.trustEmbeddedObjects === false ? "" : "this.id != null && "}!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
2095
+ isUnsecuredPortableObject(this) ||
2096
+ mustDereferencePortableObject(
2097
+ this, ${trustOwner}, v.id, options,
2098
+ )) &&
1809
2099
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
1810
2100
  if (options.crossOrigin === "throw") {
1811
2101
  throw new Error(
1812
2102
  "The property object's @id (" + v.id.href + ") has a different " +
1813
- "origin than the property owner's @id (" + this.id.href + "); " +
2103
+ "origin than the property owner's @id (" + ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} + "); " +
1814
2104
  "refusing to return the object. If you want to bypass this " +
1815
2105
  "check and are aware of the security implications, set the " +
1816
2106
  'crossOrigin option to "trust".'
@@ -1822,10 +2112,11 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1822
2112
  "return the object. If you want to bypass this check and are " +
1823
2113
  "aware of the security implications, set the crossOrigin option " +
1824
2114
  'to "trust".',
1825
- { objectId: v.id.href, parentObjectId: this.id.href },
2115
+ { objectId: v.id.href, parentObjectId: ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} },
1826
2116
  );
1827
2117
  return null;
1828
2118
  }
2119
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
1829
2120
  return v;
1830
2121
  }
1831
2122
  `;
@@ -1858,6 +2149,21 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1858
2149
  suppressError?: boolean,
1859
2150
  tracerProvider?: TracerProvider,
1860
2151
  crossOrigin?: "ignore" | "throw" | "trust";
2152
+ gateways?: readonly (string | URL)[];
2153
+ verifyPortableObject?: PortableObjectVerifier;
2154
+ /**
2155
+ * Whether objects that this call newly fetches use the
2156
+ * \`verifyPortableObject\` option of this call as their default
2157
+ * verifier. Defaults to \`true\`. If \`false\`, the option
2158
+ * applies to this call only, and fetched objects use this object's
2159
+ * own default verifier, if any, as their default instead. Pass
2160
+ * \`false\` for a temporary verifier, e.g., one that accepts
2161
+ * everything. Objects that were already cached in this object or
2162
+ * embedded in it keep their defaults either way, and this option
2163
+ * itself is not passed on.
2164
+ * @since 2.4.0
2165
+ */
2166
+ inheritPortableObjectVerifier?: boolean;
1861
2167
  } = {}
1862
2168
  ): AsyncIterable<${getTypeNames(property.range, types)}> {
1863
2169
  if (this._warning != null) {
@@ -1866,28 +2172,52 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1866
2172
  this._warning.values
1867
2173
  );
1868
2174
  }
2175
+ if (
2176
+ options.verifyPortableObject == null &&
2177
+ this._verifyPortableObject != null
2178
+ ) {
2179
+ options = {
2180
+ ...options,
2181
+ verifyPortableObject: this._verifyPortableObject,
2182
+ };
2183
+ }
1869
2184
  const vs = this.${await getFieldName(property.uri)};
1870
2185
  for (let i = 0; i < vs.length; i++) {
1871
2186
  let v = vs[i];
1872
2187
  if (!(v instanceof URL) &&
1873
- v.id != null &&
1874
- !isTrustedIriOrigin(options, v.id, this.id) &&
1875
2188
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
1876
- v = v.id;
2189
+ if (v.id == null) {
2190
+ if (isUnsecuredPortableObject(this)) {
2191
+ warnUnverifiableEmbeddedObject(this, ${JSON.stringify(property.uri)});
2192
+ continue;
2193
+ }
2194
+ } else if (
2195
+ !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
2196
+ isUnsecuredPortableObject(this) ||
2197
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
2198
+ ) {
2199
+ v = v.id;
2200
+ }
1877
2201
  }
1878
2202
  if (v instanceof URL) {
1879
2203
  const fetched =
1880
2204
  await this.#fetch${pascalCase(property.singularName)}(v, options);
1881
2205
  if (fetched == null) continue;
2206
+ // Not cached, so that it is verified when the portable object
2207
+ // policy applies later:
2208
+ if (isUnverifiedPortableClaim(fetched)) {
2209
+ yield fetched;
2210
+ continue;
2211
+ }
1882
2212
  vs[i] = fetched;
1883
2213
  this.${await getFieldName(property.uri, "#_trust")}.add(i);
1884
2214
  this._cachedJsonLd = undefined;
2215
+ recordPortableReferrer(this, fetched, ${JSON.stringify(property.uri)});
1885
2216
  yield fetched;
1886
2217
  continue;
1887
2218
  }
1888
2219
  `;
1889
- if (property.compactName != null) {
1890
- yield `
2220
+ if (property.compactName != null) yield `
1891
2221
  if (
1892
2222
  this._cachedJsonLd != null &&
1893
2223
  typeof this._cachedJsonLd === "object" &&
@@ -1898,26 +2228,36 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1898
2228
  ${JSON.stringify(property.compactName)}];
1899
2229
  const obj = Array.isArray(prop) ? prop[i] : prop;
1900
2230
  if (obj != null && typeof obj === "object" && "@context" in obj) {
1901
- `;
1902
- yield `
1903
- v = await this.#${property.singularName}_fromJsonLd(obj, {
1904
- ...options,
1905
- baseUrl: ${cachedPropertyBaseUrl},
1906
- });
1907
- `;
1908
- yield `
2231
+ try {
2232
+ v = await this.#${property.singularName}_fromJsonLd(obj, {
2233
+ ...options,
2234
+ // Keep the parent default for embedded objects.
2235
+ verifyPortableObject: this._verifyPortableObject,
2236
+ baseUrl: ${cachedPropertyBaseUrl},
2237
+ });
2238
+ } catch (error) {
2239
+ if (!options.suppressError) throw error;
2240
+ getLogger(["fedify", "vocab"]).debug(
2241
+ "Failed to parse embedded value of {property} at index {index}: {error}",
2242
+ { property: ${JSON.stringify(property.uri)}, index: i, error },
2243
+ );
2244
+ continue;
2245
+ }
1909
2246
  }
1910
2247
  }
1911
2248
  `;
1912
- }
1913
2249
  yield `
1914
2250
  if (v?.id != null &&
1915
- this.id != null && !isTrustedIriOrigin(options, v.id, this.id) &&
2251
+ ((${type.trustEmbeddedObjects === false ? "" : "this.id != null && "}!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
2252
+ isUnsecuredPortableObject(this) ||
2253
+ mustDereferencePortableObject(
2254
+ this, ${trustOwner}, v.id, options,
2255
+ )) &&
1916
2256
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
1917
2257
  if (options.crossOrigin === "throw") {
1918
2258
  throw new Error(
1919
2259
  "The property object's @id (" + v.id.href + ") has a different " +
1920
- "origin than the property owner's @id (" + this.id.href + "); " +
2260
+ "origin than the property owner's @id (" + ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} + "); " +
1921
2261
  "refusing to return the object. If you want to bypass this " +
1922
2262
  "check and are aware of the security implications, set the " +
1923
2263
  'crossOrigin option to "trust".'
@@ -1929,10 +2269,11 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1929
2269
  "return the object. If you want to bypass this check and are " +
1930
2270
  "aware of the security implications, set the crossOrigin " +
1931
2271
  'option to "trust".',
1932
- { objectId: v.id.href, parentObjectId: this.id.href },
2272
+ { objectId: v.id.href, parentObjectId: ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} },
1933
2273
  );
1934
2274
  continue;
1935
2275
  }
2276
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
1936
2277
  yield v;
1937
2278
  }
1938
2279
  }
@@ -1950,10 +2291,35 @@ const XSD_ANY_URI = "http://www.w3.org/2001/XMLSchema#anyURI";
1950
2291
  const FEDIFY_URL = "fedify:url";
1951
2292
  const INTERNAL_RUNTIME_IMPORTS = [
1952
2293
  "compactJsonLdCache",
2294
+ "createScopedContextLoader",
1953
2295
  "getJsonLdContext",
2296
+ "getPortableActorGateways",
1954
2297
  "isTrustedIriOrigin",
1955
2298
  "normalizeJsonLdIris"
1956
2299
  ].join(",\n ");
2300
+ const PORTABLE_DEREFERENCE_IMPORTS = [
2301
+ "copyPortableProvenance",
2302
+ "createSnapshotContextLoader",
2303
+ "dereferencePortableIri",
2304
+ "getPortableResponseClaim",
2305
+ "getReferrerGateways",
2306
+ "isPortableIri",
2307
+ "isPortableMode",
2308
+ "isUnsecuredPortableObject",
2309
+ "isUnverifiedPortableClaim",
2310
+ "markUnverifiedPortableClaim",
2311
+ "mustDereferencePortableObject",
2312
+ "parseCompatibleEf61Reference",
2313
+ "type PortableResponseClaim",
2314
+ "recordPortableReferrer",
2315
+ "rejectMalformedCompatibleReference",
2316
+ "warnUnverifiableEmbeddedObject"
2317
+ ].join(",\n ");
2318
+ const SIGNED_REPRESENTATION_IMPORTS = [
2319
+ "enterSignedValueScope",
2320
+ "resolveSignedValues",
2321
+ "retainedSignedValueRef"
2322
+ ].join(",\n ");
1957
2323
  const RUNTIME_IMPORTS = [
1958
2324
  "canParseDecimal",
1959
2325
  "decodeMultibase",
@@ -1973,6 +2339,7 @@ const RUNTIME_IMPORTS = [
1973
2339
  "parseGatewayUrl",
1974
2340
  "parseIri",
1975
2341
  "parseJsonLdId",
2342
+ "type PortableObjectVerifier",
1976
2343
  "type RemoteDocument"
1977
2344
  ].join(",\n ");
1978
2345
  /**
@@ -2008,6 +2375,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2008
2375
  readonly #documentLoader?: DocumentLoader;
2009
2376
  readonly #contextLoader?: DocumentLoader;
2010
2377
  readonly #tracerProvider?: TracerProvider;
2378
+ readonly #verifyPortableObject?: PortableObjectVerifier;
2011
2379
  readonly #warning?: {
2012
2380
  category: string[];
2013
2381
  message: string;
@@ -2015,6 +2383,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2015
2383
  };
2016
2384
  #cachedJsonLd?: unknown;
2017
2385
  readonly #_baseUrl?: URL;
2386
+ #shouldCacheJsonLd = true;
2018
2387
  readonly id: URL | null;
2019
2388
 
2020
2389
  protected get _documentLoader(): DocumentLoader | undefined {
@@ -2029,6 +2398,10 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2029
2398
  return this.#tracerProvider;
2030
2399
  }
2031
2400
 
2401
+ protected get _verifyPortableObject(): PortableObjectVerifier | undefined {
2402
+ return this.#verifyPortableObject;
2403
+ }
2404
+
2032
2405
  protected get _warning(): {
2033
2406
  category: string[];
2034
2407
  message: string;
@@ -2048,6 +2421,20 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2048
2421
  protected get _baseUrl(): URL | undefined {
2049
2422
  return this.#_baseUrl;
2050
2423
  }
2424
+
2425
+ protected get _shouldCacheJsonLd(): boolean {
2426
+ return this.#shouldCacheJsonLd;
2427
+ }
2428
+
2429
+ protected set _shouldCacheJsonLd(value: boolean) {
2430
+ this.#shouldCacheJsonLd = value;
2431
+ }
2432
+
2433
+ protected static _shouldCacheDecodedJsonLd(value: unknown): boolean {
2434
+ if (value == null || typeof value !== "object") return true;
2435
+ if (!("_shouldCacheJsonLd" in value)) return true;
2436
+ return (value as { _shouldCacheJsonLd: boolean })._shouldCacheJsonLd;
2437
+ }
2051
2438
  `;
2052
2439
  yield `
2053
2440
  /**
@@ -2134,11 +2521,69 @@ async function* generateClasses(types) {
2134
2521
  from "@opentelemetry/api";\n`;
2135
2522
  yield `import {\n ${RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime";\n`;
2136
2523
  yield `import {\n ${INTERNAL_RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime/internal/jsonld-cache";\n`;
2524
+ yield `import {\n ${PORTABLE_DEREFERENCE_IMPORTS}\n} from "@fedify/vocab-runtime/internal/portable-dereference";\n`;
2525
+ yield `import {\n ${SIGNED_REPRESENTATION_IMPORTS}\n} from "@fedify/vocab-runtime/internal/signed-representation";\n`;
2137
2526
  yield `import {
2138
2527
  isTemporalDuration,
2139
2528
  isTemporalInstant,
2140
2529
  } from "@fedify/vocab-runtime/temporal";\n`;
2141
- const portableIriKeys = new Set(["@id", "id"]);
2530
+ yield `
2531
+ function canDecodeIri(iri: string): boolean {
2532
+ try { parseIri(iri); return true; } catch { return iri.startsWith("at://"); }
2533
+ }
2534
+
2535
+ function decodeIri(iri: string): URL {
2536
+ return !URL.canParse(iri) && iri.startsWith("at://")
2537
+ ? new URL("at://" + encodeURIComponent(iri.substring(5)))
2538
+ : parseIri(iri);
2539
+ }
2540
+
2541
+ function inspectIri(iri: URL): string {
2542
+ // Inspecting an object must not throw, even for a URL that cannot be
2543
+ // formatted as an IRI, e.g., a malformed portable ID:
2544
+ try { return formatIri(iri); } catch { return iri.href; }
2545
+ }
2546
+ `;
2547
+ yield `
2548
+ function isValidLanguageTag(language: string): boolean {
2549
+ try {
2550
+ new Intl.Locale(language);
2551
+ return true;
2552
+ } catch (error) {
2553
+ if (error instanceof RangeError) return false;
2554
+ throw error;
2555
+ }
2556
+ }
2557
+ `;
2558
+ const extraContexts = Object.fromEntries(Object.values(types).flatMap((type) => type.properties.flatMap((property) => property.extraContext == null ? [] : [[property.uri, property.extraContext]])));
2559
+ if (Object.keys(extraContexts).length > 0) yield `
2560
+ const extraPropertyContexts: Readonly<Record<string, string>> =
2561
+ ${JSON.stringify(extraContexts)};
2562
+
2563
+ function getExtraContexts(document: unknown): string[] {
2564
+ const contexts = new Set<string>();
2565
+ const pending: unknown[] = [document];
2566
+ while (pending.length > 0) {
2567
+ const value = pending.pop();
2568
+ if (value == null || typeof value !== "object") continue;
2569
+ if (Array.isArray(value)) {
2570
+ for (const item of value) pending.push(item);
2571
+ continue;
2572
+ }
2573
+ if ("@value" in value) continue;
2574
+ for (const [key, child] of globalThis.Object.entries(value)) {
2575
+ if (key === "@context") continue;
2576
+ if (globalThis.Object.hasOwn(extraPropertyContexts, key)) {
2577
+ contexts.add(extraPropertyContexts[key]);
2578
+ }
2579
+ pending.push(child);
2580
+ }
2581
+ }
2582
+ return [...contexts];
2583
+ }
2584
+ `;
2585
+ yield "\n\n";
2586
+ const portableIriKeys = /* @__PURE__ */ new Set(["@id", "id"]);
2142
2587
  for (const type of Object.values(types)) for (const property of type.properties) addPortableIriKeys(portableIriKeys, property, types);
2143
2588
  yield "const PORTABLE_IRI_PATTERN = /^ap(?:\\+ef61)?:\\/\\//i;\n";
2144
2589
  yield `const PORTABLE_IRI_KEYS: ReadonlySet<string> = new Set(${JSON.stringify([...portableIriKeys].sort())});\n\n`;