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

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.1";
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"])`;
@@ -302,7 +303,7 @@ const scalarTypes = {
302
303
  && typeof ${v}["@value"] === "string" && !("@language" in ${v})`;
303
304
  },
304
305
  decoder(v) {
305
- return `new Intl.Locale(${v}["@value"])`;
306
+ return `new Intl.Locale(normalizeLanguageTag(${v}["@value"]))`;
306
307
  }
307
308
  },
308
309
  "fedify:url": {
@@ -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) {
@@ -1290,7 +1383,7 @@ async function* generateConstructor(typeUri, types) {
1290
1383
  if (Array.isArray(values.${property.pluralName}) &&
1291
1384
  values.${property.pluralName}.every(v => ${typeGuards})) {
1292
1385
  // @ts-ignore: type is checked above.
1293
- this.${fieldName} = values.${property.pluralName};
1386
+ this.${fieldName} = values.${property.pluralName}.slice();
1294
1387
  `;
1295
1388
  if (!allScalarTypes) yield `
1296
1389
  for (let i = 0; i < values.${property.pluralName}.length; i++) {
@@ -1315,9 +1408,17 @@ 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
- * @returns The cloned instance.
1420
+ * @returns The cloned instance with its own property arrays. Nested objects
1421
+ * and URLs are shared with this instance.
1321
1422
  */
1322
1423
  ${emitOverride$1(typeUri, types)} clone(
1323
1424
  values:
@@ -1328,6 +1429,7 @@ async function* generateCloner(typeUri, types) {
1328
1429
  options: {
1329
1430
  documentLoader?: DocumentLoader,
1330
1431
  contextLoader?: DocumentLoader,
1432
+ verifyPortableObject?: PortableObjectVerifier,
1331
1433
  } = {}
1332
1434
  ): ${type.name} {
1333
1435
  if (this._warning != null) {
@@ -1345,13 +1447,14 @@ async function* generateCloner(typeUri, types) {
1345
1447
  { id: values.id ?? this.id },
1346
1448
  options
1347
1449
  );
1450
+ copyPortableProvenance(this, clone);
1348
1451
  `;
1349
1452
  else yield `const clone = super.clone(values, options) as unknown as ${type.name};`;
1350
1453
  for (const property of type.properties) {
1351
1454
  const fieldName = await getFieldName(property.uri);
1352
1455
  const trustFieldName = await getFieldName(property.uri, "#_trust");
1353
1456
  const allScalarTypes = areAllScalarTypes(property.range, types);
1354
- yield `clone.${fieldName} = this.${fieldName};`;
1457
+ yield `clone.${fieldName} = this.${fieldName}.slice();`;
1355
1458
  if (!allScalarTypes) yield `clone.${trustFieldName} = new Set(this.${trustFieldName});`;
1356
1459
  if (hasSingularAccessor(property)) {
1357
1460
  let typeGuards = getTypeGuards(property.range, types, `values.${property.singularName}`);
@@ -1402,7 +1505,7 @@ async function* generateCloner(typeUri, types) {
1402
1505
  if (Array.isArray(values.${property.pluralName}) &&
1403
1506
  values.${property.pluralName}.every(v => ${typeGuards})) {
1404
1507
  // @ts-ignore: type is checked above.
1405
- clone.${fieldName} = values.${property.pluralName};
1508
+ clone.${fieldName} = values.${property.pluralName}.slice();
1406
1509
  `;
1407
1510
  if (!allScalarTypes) yield `
1408
1511
  clone.${trustFieldName} = new Set();
@@ -1440,12 +1543,12 @@ async function* generateInspector(typeUri, types) {
1440
1543
  [Symbol.for("Deno.customInspect")]: (
1441
1544
  inspect: typeof Deno.inspect,
1442
1545
  options: Deno.InspectOptions,
1443
- ): string => "URL " + inspect(this.id!.href, options),
1546
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
1444
1547
  [Symbol.for("nodejs.util.inspect.custom")]: (
1445
1548
  _depth: number,
1446
1549
  options: unknown,
1447
1550
  inspect: (value: unknown, options: unknown) => string,
1448
- ): string => "URL " + inspect(this.id!.href, options),
1551
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
1449
1552
  };
1450
1553
  }
1451
1554
  `;
@@ -1461,12 +1564,12 @@ async function* generateInspector(typeUri, types) {
1461
1564
  [Symbol.for("Deno.customInspect")]: (
1462
1565
  inspect: typeof Deno.inspect,
1463
1566
  options: Deno.InspectOptions,
1464
- ): string => "URL " + inspect(v.href, options),
1567
+ ): string => "URL " + inspect(inspectIri(v), options),
1465
1568
  [Symbol.for("nodejs.util.inspect.custom")]: (
1466
1569
  _depth: number,
1467
1570
  options: unknown,
1468
1571
  inspect: (value: unknown, options: unknown) => string,
1469
- ): string => "URL " + inspect(v.href, options),
1572
+ ): string => "URL " + inspect(inspectIri(v), options),
1470
1573
  }
1471
1574
  : v);
1472
1575
  `;
@@ -1536,6 +1639,7 @@ function emitOverride(typeUri, types, property) {
1536
1639
  }
1537
1640
  async function* generateProperty(type, property, types, moduleVarNames) {
1538
1641
  const override = emitOverride(type.uri, types, property);
1642
+ const trustOwner = type.trustEmbeddedObjects === false ? "null" : "this.id";
1539
1643
  const doc = `\n/** ${property.description.replaceAll("\n", "\n * ")}\n */\n`;
1540
1644
  const cachedPropertyBaseUrl = `(options as { baseUrl?: URL }).baseUrl ??
1541
1645
  this._baseUrl ??
@@ -1576,6 +1680,9 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1576
1680
  suppressError?: boolean,
1577
1681
  tracerProvider?: TracerProvider,
1578
1682
  crossOrigin?: "ignore" | "throw" | "trust";
1683
+ gateways?: readonly (string | URL)[];
1684
+ verifyPortableObject?: PortableObjectVerifier;
1685
+ inheritPortableObjectVerifier?: boolean;
1579
1686
  } = {},
1580
1687
  ): Promise<${getTypeNames(property.range, types)} | null> {
1581
1688
  const documentLoader =
@@ -1588,11 +1695,96 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1588
1695
  ${JSON.stringify(name)},
1589
1696
  ${JSON.stringify(version)},
1590
1697
  );
1698
+ // The fetched object uses the verifier given to this call (which falls
1699
+ // back to the parent's own default) for its accessors by default,
1700
+ // unless the call keeps its verifier to itself:
1701
+ const childVerifier = options.inheritPortableObjectVerifier === false
1702
+ ? this._verifyPortableObject
1703
+ : options.verifyPortableObject;
1591
1704
  return await tracer.startActiveSpan("activitypub.lookup_object", async (span) => {
1705
+ const dereferencePortable = async (
1706
+ portableUrl: URL,
1707
+ extra: {
1708
+ inferredGateways?: readonly URL[];
1709
+ response?: RemoteDocument;
1710
+ contextLoader?: DocumentLoader;
1711
+ } = {},
1712
+ ) => {
1713
+ try {
1714
+ const obj = await dereferencePortableIri(portableUrl, {
1715
+ documentLoader,
1716
+ contextLoader: extra.contextLoader ?? contextLoader,
1717
+ tracerProvider,
1718
+ gateways: options.gateways,
1719
+ inferredGateways: extra.inferredGateways,
1720
+ response: extra.response,
1721
+ verifyPortableObject: options.verifyPortableObject,
1722
+ referrer: {
1723
+ object: this,
1724
+ property: ${JSON.stringify(property.uri)},
1725
+ },
1726
+ suppressError: options.suppressError,
1727
+ crossOrigin: options.crossOrigin,
1728
+ span,
1729
+ parse: (document, parseOptions) =>
1730
+ this.#${property.singularName}_fromJsonLd(document, {
1731
+ documentLoader,
1732
+ contextLoader: parseOptions.contextLoader,
1733
+ tracerProvider,
1734
+ verifyPortableObject: childVerifier,
1735
+ baseUrl: parseOptions.baseUrl,
1736
+ }),
1737
+ });
1738
+ if (obj != null) {
1739
+ span.setAttribute(
1740
+ "activitypub.object.id",
1741
+ (obj.id ?? portableUrl).href,
1742
+ );
1743
+ span.setAttribute(
1744
+ "activitypub.object.type",
1745
+ // @ts-ignore: obj.constructor always has a typeId.
1746
+ obj.constructor.typeId.href
1747
+ );
1748
+ }
1749
+ return obj;
1750
+ } catch (e) {
1751
+ span.setStatus({
1752
+ code: SpanStatusCode.ERROR,
1753
+ message: String(e),
1754
+ });
1755
+ throw e;
1756
+ } finally {
1757
+ span.end();
1758
+ }
1759
+ };
1760
+ if (isPortableIri(url)) {
1761
+ // A reference in a portable actor's own document, e.g., its outbox,
1762
+ // has no location hints, so the actor's gateways are used instead:
1763
+ return await dereferencePortable(
1764
+ url,
1765
+ options.gateways == null
1766
+ ? { inferredGateways: getReferrerGateways(this, url) }
1767
+ : {},
1768
+ );
1769
+ }
1592
1770
  const lookupUrl = formatIri(url);
1771
+ const portableMode = isPortableMode(this, options);
1772
+ if (portableMode) {
1773
+ const compatible = parseCompatibleEf61Reference(url);
1774
+ if (compatible === null) {
1775
+ span.end();
1776
+ return rejectMalformedCompatibleReference(lookupUrl, options);
1777
+ } else if (compatible != null) {
1778
+ return await dereferencePortable(compatible.id, {
1779
+ inferredGateways: [compatible.gateway],
1780
+ });
1781
+ }
1782
+ }
1593
1783
  let fetchResult: RemoteDocument;
1594
1784
  try {
1595
- fetchResult = await documentLoader(lookupUrl);
1785
+ fetchResult = await documentLoader(lookupUrl, {
1786
+ suppressError: options.suppressError,
1787
+ });
1596
1788
  } catch (error) {
1597
1789
  span.setStatus({
1598
1790
  code: SpanStatusCode.ERROR,
@@ -1600,7 +1792,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1600
1792
  });
1601
1793
  span.end();
1602
1794
  if (options.suppressError) {
1603
- getLogger(["fedify", "vocab"]).error(
1795
+ getLogger(["fedify", "vocab"]).warn(
1604
1796
  "Failed to fetch {url}: {error}",
1605
1797
  { error, url: lookupUrl }
1606
1798
  );
@@ -1609,52 +1801,96 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1609
1801
  throw error;
1610
1802
  }
1611
1803
  const { document, documentUrl } = fetchResult;
1612
- const baseUrl = parseIri(documentUrl);
1804
+ // In portable mode, the document may turn out to be a portable object,
1805
+ // which has to be verified with the same context documents:
1806
+ const snapshot = portableMode
1807
+ ? createSnapshotContextLoader(contextLoader, options.suppressError)
1808
+ : null;
1809
+ const scopedContext = createScopedContextLoader(
1810
+ snapshot?.loader ?? contextLoader,
1811
+ snapshot == null && options.suppressError,
1812
+ );
1613
1813
  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".'
1814
+ let claim: PortableResponseClaim | null | undefined;
1815
+ try {
1816
+ const baseUrl = parseIri(documentUrl);
1817
+ const obj = await this.#${property.singularName}_fromJsonLd(
1818
+ document,
1819
+ {
1820
+ documentLoader,
1821
+ contextLoader: scopedContext.loader,
1822
+ tracerProvider,
1823
+ verifyPortableObject: childVerifier,
1824
+ baseUrl,
1825
+ }
1826
+ );
1827
+ if (snapshot == null) {
1828
+ markUnverifiedPortableClaim(obj, url, documentUrl);
1829
+ } else {
1830
+ claim = getPortableResponseClaim(documentUrl, obj.id);
1831
+ }
1832
+ if (claim === undefined) {
1833
+ if (
1834
+ obj?.id != null &&
1835
+ !isTrustedIriOrigin(options, obj.id, baseUrl)
1836
+ ) {
1837
+ if (options.crossOrigin === "throw") {
1838
+ throw new Error(
1839
+ "The object's @id (" + obj.id.href + ") has a different origin " +
1840
+ "than the document URL (" + baseUrl.href + "); refusing to return " +
1841
+ "the object. If you want to bypass this check and are aware of" +
1842
+ 'the security implications, set the crossOrigin option to "trust".'
1843
+ );
1844
+ }
1845
+ getLogger(["fedify", "vocab"]).warn(
1846
+ "The object's @id ({objectId}) has a different origin than the document " +
1847
+ "URL ({documentUrl}); refusing to return the object. If you want to " +
1848
+ "bypass this check and are aware of the security implications, " +
1849
+ 'set the crossOrigin option to "trust".',
1850
+ { ...fetchResult, objectId: obj.id.href },
1851
+ );
1852
+ span.end();
1853
+ return null;
1854
+ }
1855
+ span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
1856
+ span.setAttribute(
1857
+ "activitypub.object.type",
1858
+ // @ts-ignore: obj.constructor always has a typeId.
1859
+ obj.constructor.typeId.href
1625
1860
  );
1861
+ span.end();
1862
+ return obj;
1626
1863
  }
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;
1864
+ } catch (e) {
1865
+ if (options.suppressError) {
1866
+ getLogger(["fedify", "vocab"]).warn(
1867
+ "Failed to parse {url}: {error}",
1868
+ { error: e, url: lookupUrl }
1869
+ );
1870
+ span.end();
1871
+ return null;
1872
+ }
1873
+ span.setStatus({
1874
+ code: SpanStatusCode.ERROR,
1875
+ message: String(e),
1876
+ });
1877
+ span.end();
1878
+ throw e;
1635
1879
  }
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;
1880
+ // The document stands for a portable object, so it is verified as
1881
+ // one instead of being trusted because of where it came from:
1882
+ if (claim === null) {
1883
+ span.end();
1884
+ return rejectMalformedCompatibleReference(documentUrl, options);
1650
1885
  }
1651
- span.setStatus({
1652
- code: SpanStatusCode.ERROR,
1653
- message: String(e),
1886
+ return await dereferencePortable(claim.id, {
1887
+ inferredGateways: claim.inferredGateways,
1888
+ response: fetchResult,
1889
+ contextLoader: snapshot?.loader,
1654
1890
  });
1655
- throw e;
1656
1891
  } finally {
1657
- span.end();
1892
+ scopedContext.release();
1893
+ snapshot?.release();
1658
1894
  }
1659
1895
  });
1660
1896
  }
@@ -1665,6 +1901,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1665
1901
  documentLoader?: DocumentLoader,
1666
1902
  contextLoader?: DocumentLoader,
1667
1903
  tracerProvider?: TracerProvider,
1904
+ verifyPortableObject?: PortableObjectVerifier,
1668
1905
  baseUrl?: URL
1669
1906
  }
1670
1907
  ): Promise<${getTypeNames(property.range, types)}> {
@@ -1675,6 +1912,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1675
1912
  const tracerProvider = options.tracerProvider ??
1676
1913
  this._tracerProvider ?? trace.getTracerProvider();
1677
1914
  const baseUrl = options.baseUrl;
1915
+ const verifyPortableObject = options.verifyPortableObject;
1678
1916
  `;
1679
1917
  if (property.preprocessors != null && property.preprocessors.length > 0) {
1680
1918
  yield `
@@ -1694,6 +1932,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1694
1932
  documentLoader,
1695
1933
  contextLoader,
1696
1934
  tracerProvider,
1935
+ verifyPortableObject,
1697
1936
  baseUrl,
1698
1937
  });
1699
1938
  if (_result instanceof Error) throw _result;
@@ -1712,7 +1951,13 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1712
1951
  try {
1713
1952
  return await ${types[range].name}.fromJsonLd(
1714
1953
  jsonLd,
1715
- { documentLoader, contextLoader, tracerProvider, baseUrl },
1954
+ {
1955
+ documentLoader,
1956
+ contextLoader,
1957
+ tracerProvider,
1958
+ verifyPortableObject,
1959
+ baseUrl,
1960
+ },
1716
1961
  );
1717
1962
  } catch (e) {
1718
1963
  if (!(e instanceof TypeError)) throw e;
@@ -1754,6 +1999,21 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1754
1999
  suppressError?: boolean,
1755
2000
  tracerProvider?: TracerProvider,
1756
2001
  crossOrigin?: "ignore" | "throw" | "trust";
2002
+ gateways?: readonly (string | URL)[];
2003
+ verifyPortableObject?: PortableObjectVerifier;
2004
+ /**
2005
+ * Whether objects that this call newly fetches use the
2006
+ * \`verifyPortableObject\` option of this call as their default
2007
+ * verifier. Defaults to \`true\`. If \`false\`, the option
2008
+ * applies to this call only, and fetched objects use this object's
2009
+ * own default verifier, if any, as their default instead. Pass
2010
+ * \`false\` for a temporary verifier, e.g., one that accepts
2011
+ * everything. Objects that were already cached in this object or
2012
+ * embedded in it keep their defaults either way, and this option
2013
+ * itself is not passed on.
2014
+ * @since 2.4.0
2015
+ */
2016
+ inheritPortableObjectVerifier?: boolean;
1757
2017
  } = {}
1758
2018
  ): Promise<${getTypeNames(property.range, types)} | null> {
1759
2019
  if (this._warning != null) {
@@ -1763,25 +2023,46 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1763
2023
  );
1764
2024
  }
1765
2025
  if (this.${await getFieldName(property.uri)}.length < 1) return null;
2026
+ if (
2027
+ options.verifyPortableObject == null &&
2028
+ this._verifyPortableObject != null
2029
+ ) {
2030
+ options = {
2031
+ ...options,
2032
+ verifyPortableObject: this._verifyPortableObject,
2033
+ };
2034
+ }
1766
2035
  let v = this.${await getFieldName(property.uri)}[0];
1767
2036
  if (!(v instanceof URL) &&
1768
- v.id != null &&
1769
- !isTrustedIriOrigin(options, v.id, this.id) &&
1770
2037
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
1771
- v = v.id;
2038
+ if (v.id == null) {
2039
+ if (isUnsecuredPortableObject(this)) {
2040
+ warnUnverifiableEmbeddedObject(this, ${JSON.stringify(property.uri)});
2041
+ return null;
2042
+ }
2043
+ } else if (
2044
+ !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
2045
+ isUnsecuredPortableObject(this) ||
2046
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
2047
+ ) {
2048
+ v = v.id;
2049
+ }
1772
2050
  }
1773
2051
  if (v instanceof URL) {
1774
2052
  const fetched =
1775
2053
  await this.#fetch${(0, es_toolkit.pascalCase)(property.singularName)}(v, options);
1776
2054
  if (fetched == null) return null;
2055
+ // Not cached, so that it is verified when the portable object
2056
+ // policy applies later:
2057
+ if (isUnverifiedPortableClaim(fetched)) return fetched;
1777
2058
  this.${await getFieldName(property.uri)}[0] = fetched;
1778
2059
  this.${await getFieldName(property.uri, "#_trust")}.add(0);
1779
2060
  this._cachedJsonLd = undefined;
2061
+ recordPortableReferrer(this, fetched, ${JSON.stringify(property.uri)});
1780
2062
  return fetched;
1781
2063
  }
1782
2064
  `;
1783
- if (property.compactName != null) {
1784
- yield `
2065
+ if (property.compactName != null) yield `
1785
2066
  if (
1786
2067
  this._cachedJsonLd != null &&
1787
2068
  typeof this._cachedJsonLd === "object" &&
@@ -1792,26 +2073,36 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1792
2073
  ${JSON.stringify(property.compactName)}];
1793
2074
  const doc = Array.isArray(prop) ? prop[0] : prop;
1794
2075
  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 `
2076
+ try {
2077
+ v = await this.#${property.singularName}_fromJsonLd(doc, {
2078
+ ...options,
2079
+ // Keep the parent default for embedded objects.
2080
+ verifyPortableObject: this._verifyPortableObject,
2081
+ baseUrl: ${cachedPropertyBaseUrl},
2082
+ });
2083
+ } catch (error) {
2084
+ if (!options.suppressError) throw error;
2085
+ getLogger(["fedify", "vocab"]).debug(
2086
+ "Failed to parse embedded value of {property}: {error}",
2087
+ { property: ${JSON.stringify(property.uri)}, error },
2088
+ );
2089
+ return null;
2090
+ }
1803
2091
  }
1804
2092
  }
1805
2093
  `;
1806
- }
1807
2094
  yield `
1808
2095
  if (v?.id != null &&
1809
- this.id != null && !isTrustedIriOrigin(options, v.id, this.id) &&
2096
+ ((${type.trustEmbeddedObjects === false ? "" : "this.id != null && "}!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
2097
+ isUnsecuredPortableObject(this) ||
2098
+ mustDereferencePortableObject(
2099
+ this, ${trustOwner}, v.id, options,
2100
+ )) &&
1810
2101
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
1811
2102
  if (options.crossOrigin === "throw") {
1812
2103
  throw new Error(
1813
2104
  "The property object's @id (" + v.id.href + ") has a different " +
1814
- "origin than the property owner's @id (" + this.id.href + "); " +
2105
+ "origin than the property owner's @id (" + ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} + "); " +
1815
2106
  "refusing to return the object. If you want to bypass this " +
1816
2107
  "check and are aware of the security implications, set the " +
1817
2108
  'crossOrigin option to "trust".'
@@ -1823,10 +2114,11 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1823
2114
  "return the object. If you want to bypass this check and are " +
1824
2115
  "aware of the security implications, set the crossOrigin option " +
1825
2116
  'to "trust".',
1826
- { objectId: v.id.href, parentObjectId: this.id.href },
2117
+ { objectId: v.id.href, parentObjectId: ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} },
1827
2118
  );
1828
2119
  return null;
1829
2120
  }
2121
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
1830
2122
  return v;
1831
2123
  }
1832
2124
  `;
@@ -1859,6 +2151,21 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1859
2151
  suppressError?: boolean,
1860
2152
  tracerProvider?: TracerProvider,
1861
2153
  crossOrigin?: "ignore" | "throw" | "trust";
2154
+ gateways?: readonly (string | URL)[];
2155
+ verifyPortableObject?: PortableObjectVerifier;
2156
+ /**
2157
+ * Whether objects that this call newly fetches use the
2158
+ * \`verifyPortableObject\` option of this call as their default
2159
+ * verifier. Defaults to \`true\`. If \`false\`, the option
2160
+ * applies to this call only, and fetched objects use this object's
2161
+ * own default verifier, if any, as their default instead. Pass
2162
+ * \`false\` for a temporary verifier, e.g., one that accepts
2163
+ * everything. Objects that were already cached in this object or
2164
+ * embedded in it keep their defaults either way, and this option
2165
+ * itself is not passed on.
2166
+ * @since 2.4.0
2167
+ */
2168
+ inheritPortableObjectVerifier?: boolean;
1862
2169
  } = {}
1863
2170
  ): AsyncIterable<${getTypeNames(property.range, types)}> {
1864
2171
  if (this._warning != null) {
@@ -1867,28 +2174,52 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1867
2174
  this._warning.values
1868
2175
  );
1869
2176
  }
2177
+ if (
2178
+ options.verifyPortableObject == null &&
2179
+ this._verifyPortableObject != null
2180
+ ) {
2181
+ options = {
2182
+ ...options,
2183
+ verifyPortableObject: this._verifyPortableObject,
2184
+ };
2185
+ }
1870
2186
  const vs = this.${await getFieldName(property.uri)};
1871
2187
  for (let i = 0; i < vs.length; i++) {
1872
2188
  let v = vs[i];
1873
2189
  if (!(v instanceof URL) &&
1874
- v.id != null &&
1875
- !isTrustedIriOrigin(options, v.id, this.id) &&
1876
2190
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
1877
- v = v.id;
2191
+ if (v.id == null) {
2192
+ if (isUnsecuredPortableObject(this)) {
2193
+ warnUnverifiableEmbeddedObject(this, ${JSON.stringify(property.uri)});
2194
+ continue;
2195
+ }
2196
+ } else if (
2197
+ !isTrustedIriOrigin(options, v.id, ${trustOwner}) ||
2198
+ isUnsecuredPortableObject(this) ||
2199
+ mustDereferencePortableObject(this, ${trustOwner}, v.id, options)
2200
+ ) {
2201
+ v = v.id;
2202
+ }
1878
2203
  }
1879
2204
  if (v instanceof URL) {
1880
2205
  const fetched =
1881
2206
  await this.#fetch${(0, es_toolkit.pascalCase)(property.singularName)}(v, options);
1882
2207
  if (fetched == null) continue;
2208
+ // Not cached, so that it is verified when the portable object
2209
+ // policy applies later:
2210
+ if (isUnverifiedPortableClaim(fetched)) {
2211
+ yield fetched;
2212
+ continue;
2213
+ }
1883
2214
  vs[i] = fetched;
1884
2215
  this.${await getFieldName(property.uri, "#_trust")}.add(i);
1885
2216
  this._cachedJsonLd = undefined;
2217
+ recordPortableReferrer(this, fetched, ${JSON.stringify(property.uri)});
1886
2218
  yield fetched;
1887
2219
  continue;
1888
2220
  }
1889
2221
  `;
1890
- if (property.compactName != null) {
1891
- yield `
2222
+ if (property.compactName != null) yield `
1892
2223
  if (
1893
2224
  this._cachedJsonLd != null &&
1894
2225
  typeof this._cachedJsonLd === "object" &&
@@ -1899,26 +2230,36 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1899
2230
  ${JSON.stringify(property.compactName)}];
1900
2231
  const obj = Array.isArray(prop) ? prop[i] : prop;
1901
2232
  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 `
2233
+ try {
2234
+ v = await this.#${property.singularName}_fromJsonLd(obj, {
2235
+ ...options,
2236
+ // Keep the parent default for embedded objects.
2237
+ verifyPortableObject: this._verifyPortableObject,
2238
+ baseUrl: ${cachedPropertyBaseUrl},
2239
+ });
2240
+ } catch (error) {
2241
+ if (!options.suppressError) throw error;
2242
+ getLogger(["fedify", "vocab"]).debug(
2243
+ "Failed to parse embedded value of {property} at index {index}: {error}",
2244
+ { property: ${JSON.stringify(property.uri)}, index: i, error },
2245
+ );
2246
+ continue;
2247
+ }
1910
2248
  }
1911
2249
  }
1912
2250
  `;
1913
- }
1914
2251
  yield `
1915
2252
  if (v?.id != null &&
1916
- this.id != null && !isTrustedIriOrigin(options, v.id, this.id) &&
2253
+ ((${type.trustEmbeddedObjects === false ? "" : "this.id != null && "}!isTrustedIriOrigin(options, v.id, ${trustOwner})) ||
2254
+ isUnsecuredPortableObject(this) ||
2255
+ mustDereferencePortableObject(
2256
+ this, ${trustOwner}, v.id, options,
2257
+ )) &&
1917
2258
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
1918
2259
  if (options.crossOrigin === "throw") {
1919
2260
  throw new Error(
1920
2261
  "The property object's @id (" + v.id.href + ") has a different " +
1921
- "origin than the property owner's @id (" + this.id.href + "); " +
2262
+ "origin than the property owner's @id (" + ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} + "); " +
1922
2263
  "refusing to return the object. If you want to bypass this " +
1923
2264
  "check and are aware of the security implications, set the " +
1924
2265
  'crossOrigin option to "trust".'
@@ -1930,10 +2271,11 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1930
2271
  "return the object. If you want to bypass this check and are " +
1931
2272
  "aware of the security implications, set the crossOrigin " +
1932
2273
  'option to "trust".',
1933
- { objectId: v.id.href, parentObjectId: this.id.href },
2274
+ { objectId: v.id.href, parentObjectId: ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} },
1934
2275
  );
1935
2276
  continue;
1936
2277
  }
2278
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
1937
2279
  yield v;
1938
2280
  }
1939
2281
  }
@@ -1951,10 +2293,35 @@ const XSD_ANY_URI = "http://www.w3.org/2001/XMLSchema#anyURI";
1951
2293
  const FEDIFY_URL = "fedify:url";
1952
2294
  const INTERNAL_RUNTIME_IMPORTS = [
1953
2295
  "compactJsonLdCache",
2296
+ "createScopedContextLoader",
1954
2297
  "getJsonLdContext",
2298
+ "getPortableActorGateways",
1955
2299
  "isTrustedIriOrigin",
1956
2300
  "normalizeJsonLdIris"
1957
2301
  ].join(",\n ");
2302
+ const PORTABLE_DEREFERENCE_IMPORTS = [
2303
+ "copyPortableProvenance",
2304
+ "createSnapshotContextLoader",
2305
+ "dereferencePortableIri",
2306
+ "getPortableResponseClaim",
2307
+ "getReferrerGateways",
2308
+ "isPortableIri",
2309
+ "isPortableMode",
2310
+ "isUnsecuredPortableObject",
2311
+ "isUnverifiedPortableClaim",
2312
+ "markUnverifiedPortableClaim",
2313
+ "mustDereferencePortableObject",
2314
+ "parseCompatibleEf61Reference",
2315
+ "type PortableResponseClaim",
2316
+ "recordPortableReferrer",
2317
+ "rejectMalformedCompatibleReference",
2318
+ "warnUnverifiableEmbeddedObject"
2319
+ ].join(",\n ");
2320
+ const SIGNED_REPRESENTATION_IMPORTS = [
2321
+ "enterSignedValueScope",
2322
+ "resolveSignedValues",
2323
+ "retainedSignedValueRef"
2324
+ ].join(",\n ");
1958
2325
  const RUNTIME_IMPORTS = [
1959
2326
  "canParseDecimal",
1960
2327
  "decodeMultibase",
@@ -1970,10 +2337,12 @@ const RUNTIME_IMPORTS = [
1970
2337
  "isDecimal",
1971
2338
  "isGatewayUrl",
1972
2339
  "LanguageString",
2340
+ "normalizeLanguageTag",
1973
2341
  "parseDecimal",
1974
2342
  "parseGatewayUrl",
1975
2343
  "parseIri",
1976
2344
  "parseJsonLdId",
2345
+ "type PortableObjectVerifier",
1977
2346
  "type RemoteDocument"
1978
2347
  ].join(",\n ");
1979
2348
  /**
@@ -2009,6 +2378,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2009
2378
  readonly #documentLoader?: DocumentLoader;
2010
2379
  readonly #contextLoader?: DocumentLoader;
2011
2380
  readonly #tracerProvider?: TracerProvider;
2381
+ readonly #verifyPortableObject?: PortableObjectVerifier;
2012
2382
  readonly #warning?: {
2013
2383
  category: string[];
2014
2384
  message: string;
@@ -2016,6 +2386,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2016
2386
  };
2017
2387
  #cachedJsonLd?: unknown;
2018
2388
  readonly #_baseUrl?: URL;
2389
+ #shouldCacheJsonLd = true;
2019
2390
  readonly id: URL | null;
2020
2391
 
2021
2392
  protected get _documentLoader(): DocumentLoader | undefined {
@@ -2030,6 +2401,10 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2030
2401
  return this.#tracerProvider;
2031
2402
  }
2032
2403
 
2404
+ protected get _verifyPortableObject(): PortableObjectVerifier | undefined {
2405
+ return this.#verifyPortableObject;
2406
+ }
2407
+
2033
2408
  protected get _warning(): {
2034
2409
  category: string[];
2035
2410
  message: string;
@@ -2049,6 +2424,20 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2049
2424
  protected get _baseUrl(): URL | undefined {
2050
2425
  return this.#_baseUrl;
2051
2426
  }
2427
+
2428
+ protected get _shouldCacheJsonLd(): boolean {
2429
+ return this.#shouldCacheJsonLd;
2430
+ }
2431
+
2432
+ protected set _shouldCacheJsonLd(value: boolean) {
2433
+ this.#shouldCacheJsonLd = value;
2434
+ }
2435
+
2436
+ protected static _shouldCacheDecodedJsonLd(value: unknown): boolean {
2437
+ if (value == null || typeof value !== "object") return true;
2438
+ if (!("_shouldCacheJsonLd" in value)) return true;
2439
+ return (value as { _shouldCacheJsonLd: boolean })._shouldCacheJsonLd;
2440
+ }
2052
2441
  `;
2053
2442
  yield `
2054
2443
  /**
@@ -2135,11 +2524,69 @@ async function* generateClasses(types) {
2135
2524
  from "@opentelemetry/api";\n`;
2136
2525
  yield `import {\n ${RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime";\n`;
2137
2526
  yield `import {\n ${INTERNAL_RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime/internal/jsonld-cache";\n`;
2527
+ yield `import {\n ${PORTABLE_DEREFERENCE_IMPORTS}\n} from "@fedify/vocab-runtime/internal/portable-dereference";\n`;
2528
+ yield `import {\n ${SIGNED_REPRESENTATION_IMPORTS}\n} from "@fedify/vocab-runtime/internal/signed-representation";\n`;
2138
2529
  yield `import {
2139
2530
  isTemporalDuration,
2140
2531
  isTemporalInstant,
2141
2532
  } from "@fedify/vocab-runtime/temporal";\n`;
2142
- const portableIriKeys = new Set(["@id", "id"]);
2533
+ yield `
2534
+ function canDecodeIri(iri: string): boolean {
2535
+ try { parseIri(iri); return true; } catch { return iri.startsWith("at://"); }
2536
+ }
2537
+
2538
+ function decodeIri(iri: string): URL {
2539
+ return !URL.canParse(iri) && iri.startsWith("at://")
2540
+ ? new URL("at://" + encodeURIComponent(iri.substring(5)))
2541
+ : parseIri(iri);
2542
+ }
2543
+
2544
+ function inspectIri(iri: URL): string {
2545
+ // Inspecting an object must not throw, even for a URL that cannot be
2546
+ // formatted as an IRI, e.g., a malformed portable ID:
2547
+ try { return formatIri(iri); } catch { return iri.href; }
2548
+ }
2549
+ `;
2550
+ yield `
2551
+ function isValidLanguageTag(language: string): boolean {
2552
+ try {
2553
+ new Intl.Locale(normalizeLanguageTag(language));
2554
+ return true;
2555
+ } catch (error) {
2556
+ if (error instanceof RangeError) return false;
2557
+ throw error;
2558
+ }
2559
+ }
2560
+ `;
2561
+ const extraContexts = Object.fromEntries(Object.values(types).flatMap((type) => type.properties.flatMap((property) => property.extraContext == null ? [] : [[property.uri, property.extraContext]])));
2562
+ if (Object.keys(extraContexts).length > 0) yield `
2563
+ const extraPropertyContexts: Readonly<Record<string, string>> =
2564
+ ${JSON.stringify(extraContexts)};
2565
+
2566
+ function getExtraContexts(document: unknown): string[] {
2567
+ const contexts = new Set<string>();
2568
+ const pending: unknown[] = [document];
2569
+ while (pending.length > 0) {
2570
+ const value = pending.pop();
2571
+ if (value == null || typeof value !== "object") continue;
2572
+ if (Array.isArray(value)) {
2573
+ for (const item of value) pending.push(item);
2574
+ continue;
2575
+ }
2576
+ if ("@value" in value) continue;
2577
+ for (const [key, child] of globalThis.Object.entries(value)) {
2578
+ if (key === "@context") continue;
2579
+ if (globalThis.Object.hasOwn(extraPropertyContexts, key)) {
2580
+ contexts.add(extraPropertyContexts[key]);
2581
+ }
2582
+ pending.push(child);
2583
+ }
2584
+ }
2585
+ return [...contexts];
2586
+ }
2587
+ `;
2588
+ yield "\n\n";
2589
+ const portableIriKeys = /* @__PURE__ */ new Set(["@id", "id"]);
2143
2590
  for (const type of Object.values(types)) for (const property of type.properties) addPortableIriKeys(portableIriKeys, property, types);
2144
2591
  yield "const PORTABLE_IRI_PATTERN = /^ap(?:\\+ef61)?:\\/\\//i;\n";
2145
2592
  yield `const PORTABLE_IRI_KEYS: ReadonlySet<string> = new Set(${JSON.stringify([...portableIriKeys].sort())});\n\n`;