@fedify/vocab-tools 2.4.0-pr.936.41 → 2.4.0

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