@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.js CHANGED
@@ -7,7 +7,7 @@ import { parse } from "yaml";
7
7
  import { pascalCase } from "es-toolkit";
8
8
  //#region deno.json
9
9
  var name = "@fedify/vocab-tools";
10
- var version = "2.4.0-pr.936.41+59c7432c";
10
+ var version = "2.4.1";
11
11
  //#endregion
12
12
  //#region src/type.ts
13
13
  const HEURISTICS_CONTEXTS = [
@@ -185,7 +185,8 @@ const scalarTypes = {
185
185
  dataCheck(v) {
186
186
  return `typeof ${v} === "object" && "@language" in ${v} && "@value" in ${v}
187
187
  && typeof ${v}["@language"] === "string"
188
- && typeof ${v}["@value"] === "string"`;
188
+ && typeof ${v}["@value"] === "string"
189
+ && isValidLanguageTag(${v}["@language"])`;
189
190
  },
190
191
  decoder(v) {
191
192
  return `new LanguageString(${v}["@value"], ${v}["@language"])`;
@@ -301,7 +302,7 @@ const scalarTypes = {
301
302
  && typeof ${v}["@value"] === "string" && !("@language" in ${v})`;
302
303
  },
303
304
  decoder(v) {
304
- return `new Intl.Locale(${v}["@value"])`;
305
+ return `new Intl.Locale(normalizeLanguageTag(${v}["@value"]))`;
305
306
  }
306
307
  },
307
308
  "fedify:url": {
@@ -316,13 +317,14 @@ const scalarTypes = {
316
317
  return `formatIri(${v})`;
317
318
  },
318
319
  dataCheck(v) {
319
- return `typeof ${v} === "object" && "@value" in ${v}
320
- && typeof ${v}["@value"] === "string"
321
- && ${v}["@value"] !== "" && ${v}["@value"] !== "/"`;
320
+ return `typeof ${v} === "object" &&
321
+ ((typeof ${v}["@id"] === "string" && canDecodeIri(${v}["@id"])) ||
322
+ (typeof ${v}["@value"] === "string" && canDecodeIri(${v}["@value"])))`;
322
323
  },
323
324
  decoder(v) {
324
- return `parseIri(${v}["@value"])`;
325
- }
325
+ return `decodeIri(typeof ${v}["@id"] === "string" ? ${v}["@id"] : ${v}["@value"])`;
326
+ },
327
+ skipUnparsable: true
326
328
  },
327
329
  "fedify:gatewayUrl": {
328
330
  name: "URL",
@@ -330,10 +332,10 @@ const scalarTypes = {
330
332
  return `${v} instanceof URL && isGatewayUrl(${v})`;
331
333
  },
332
334
  encoder(v) {
333
- return `{ "@id": formatIri(${v}) }`;
335
+ return `{ "@id": ${v}.origin }`;
334
336
  },
335
337
  compactEncoder(v) {
336
- return `formatIri(${v})`;
338
+ return `${v}.origin`;
337
339
  },
338
340
  dataCheck(v) {
339
341
  return `${v} != null && typeof ${v} === "object" &&
@@ -556,6 +558,9 @@ function getDecoder(typeUri, types, variable, optionsVariable, baseUrlVariable)
556
558
  )`;
557
559
  throw new Error(`Unknown type: ${typeUri}`);
558
560
  }
561
+ function skipsUnparsable(typeUri) {
562
+ return scalarTypes[typeUri]?.skipUnparsable ?? false;
563
+ }
559
564
  function getDataCheck(typeUri, types, variable) {
560
565
  if (typeUri in scalarTypes) return scalarTypes[typeUri].dataCheck(variable);
561
566
  if (typeUri in types) {
@@ -735,6 +740,7 @@ function* generatePreprocessorBlock(property, rangeTypeName, variable, baseUrlEx
735
740
  documentLoader: options.documentLoader,
736
741
  contextLoader: options.contextLoader,
737
742
  tracerProvider: options.tracerProvider,
743
+ verifyPortableObject: options.verifyPortableObject,
738
744
  baseUrl: ${baseUrlExpr},
739
745
  });
740
746
  if (_result instanceof Error) throw _result;
@@ -783,6 +789,10 @@ async function* generateEncoder(typeUri, types) {
783
789
  ...options,
784
790
  contextLoader: options.contextLoader ?? getDocumentLoader(),
785
791
  };
792
+ // Joins the enclosing frame's signed-value scope, or starts one when this
793
+ // frame is the outermost. Only the owning frame puts retained signed
794
+ // child documents back in place of their placeholders.
795
+ const signedValues = enterSignedValueScope(options);
786
796
  `;
787
797
  if (isCompactableType(typeUri, types)) {
788
798
  yield `
@@ -813,9 +823,13 @@ async function* generateEncoder(typeUri, types) {
813
823
  for (const v of this.${await getFieldName(property.uri)}) {
814
824
  const item = (
815
825
  `;
816
- if (!areAllScalarTypes(property.range, types)) yield "v instanceof URL ? formatIri(v) : ";
826
+ if (!areAllScalarTypes(property.range, types)) {
827
+ yield "retainedSignedValueRef(v, signedValues.scope) ?? (";
828
+ yield "v instanceof URL ? formatIri(v) : ";
829
+ }
817
830
  const encoders = getEncoders(property.range, types, "v", "options", true);
818
831
  for (const code of encoders) yield code;
832
+ if (!areAllScalarTypes(property.range, types)) yield ")";
819
833
  yield `
820
834
  );
821
835
  compactItems.push(item);
@@ -846,7 +860,9 @@ async function* generateEncoder(typeUri, types) {
846
860
  ${type.typeless ? "" : `result["type"] = ${JSON.stringify(type.compactName ?? type.uri)};`}
847
861
  if (this.id != null) result["id"] = formatIri(this.id);
848
862
  result["@context"] = ${JSON.stringify(type.defaultContext)};
849
- return result;
863
+ return signedValues.owner
864
+ ? resolveSignedValues(result, signedValues.scope)
865
+ : result;
850
866
  }
851
867
  `;
852
868
  }
@@ -870,12 +886,21 @@ async function* generateEncoder(typeUri, types) {
870
886
  yield `
871
887
  array = [];
872
888
  for (const v of this.${await getFieldName(property.uri)}) {
873
- const element = (
889
+ let element = (
874
890
  `;
875
- if (!areAllScalarTypes(property.range, types)) yield "v instanceof URL ? { \"@id\": formatIri(v) } : ";
891
+ if (!areAllScalarTypes(property.range, types)) {
892
+ yield "retainedSignedValueRef(v, signedValues.scope) ?? (";
893
+ yield "v instanceof URL ? { \"@id\": formatIri(v) } : ";
894
+ }
876
895
  for (const code of getEncoders(property.range, types, "v", "options")) yield code;
896
+ if (!areAllScalarTypes(property.range, types)) yield ")";
877
897
  yield `
878
898
  );
899
+ if (Array.isArray(element)) {
900
+ if (element.length < 1) continue;
901
+ if (element.length == 1) element = element[0];
902
+ }
903
+ if (typeof element === "undefined") continue;
879
904
  `;
880
905
  if (isNonFunctionalProperty(property) && property.container === "graph") yield `array.push({ "@graph": element });`;
881
906
  else yield `array.push(element);`;
@@ -903,16 +928,35 @@ async function* generateEncoder(typeUri, types) {
903
928
  if (options.format === "expand") {
904
929
  return await jsonld.expand(
905
930
  values,
906
- { documentLoader: options.contextLoader },
931
+ {
932
+ documentLoader: options.contextLoader,
933
+ keepFreeFloatingNodes: true,
934
+ },
907
935
  );
908
936
  }
909
- const docContext = options.context ??
937
+ let docContext: Parameters<typeof jsonld.compact>[1] = options.context ??
910
938
  ${JSON.stringify(type.defaultContext)};
911
- const compacted = await jsonld.compact(
939
+ let compacted = await jsonld.compact(
912
940
  values,
913
941
  docContext,
914
942
  { documentLoader: options.contextLoader },
915
943
  );
944
+ `;
945
+ if (Object.values(types).some((t) => t.properties.some((p) => p.extraContext != null))) yield `
946
+ if (options.context == null) {
947
+ const currentContexts = Array.isArray(docContext) ? docContext : [docContext];
948
+ const additionalContexts = getExtraContexts(compacted).filter(
949
+ context => !currentContexts.includes(context)
950
+ );
951
+ if (additionalContexts.length > 0) {
952
+ docContext = [...currentContexts, ...additionalContexts];
953
+ compacted = await jsonld.compact(values, docContext, {
954
+ documentLoader: options.contextLoader,
955
+ });
956
+ }
957
+ }
958
+ `;
959
+ yield `
916
960
  if (docContext != null) {
917
961
  // Embed context
918
962
  `;
@@ -936,12 +980,14 @@ async function* generateEncoder(typeUri, types) {
936
980
  }
937
981
  yield `
938
982
  }
939
- return compacted;
983
+ return signedValues.owner
984
+ ? resolveSignedValues(compacted, signedValues.scope)
985
+ : compacted;
940
986
  }
941
987
 
942
988
  protected ${emitOverride$1(typeUri, types)} isCompactable(): boolean {
943
989
  `;
944
- for (const property of type.properties) if (!property.range.every((r) => isCompactableType(r, types))) yield `
990
+ for (const property of type.properties) if (property.extraContext != null || !property.range.every((r) => isCompactableType(r, types))) yield `
945
991
  if (
946
992
  this.${await getFieldName(property.uri)} != null &&
947
993
  this.${await getFieldName(property.uri)}.length > 0
@@ -963,6 +1009,10 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
963
1009
  * - \`contextLoader\`: The loader for remote JSON-LD contexts.
964
1010
  * - \`tracerProvider\`: The OpenTelemetry tracer provider to use.
965
1011
  * If omitted, the global tracer provider is used.
1012
+ * - \`verifyPortableObject\`: The default FEP-ef61 portable
1013
+ * object verifier for the property accessors of the
1014
+ * returned object and the objects obtained from it. It
1015
+ * does not verify the given \`json\` itself.
966
1016
  * @returns The object of this type.
967
1017
  * @throws {TypeError} If the given \`json\` is invalid.
968
1018
  */
@@ -972,6 +1022,7 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
972
1022
  documentLoader?: DocumentLoader,
973
1023
  contextLoader?: DocumentLoader,
974
1024
  tracerProvider?: TracerProvider,
1025
+ verifyPortableObject?: PortableObjectVerifier,
975
1026
  baseUrl?: URL,
976
1027
  } = {},
977
1028
  ): Promise<${type.name}> {
@@ -1010,6 +1061,7 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1010
1061
  documentLoader?: DocumentLoader,
1011
1062
  contextLoader?: DocumentLoader,
1012
1063
  tracerProvider?: TracerProvider,
1064
+ verifyPortableObject?: PortableObjectVerifier,
1013
1065
  baseUrl?: URL,
1014
1066
  } = {},
1015
1067
  ): Promise<${type.name}> {
@@ -1026,7 +1078,17 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1026
1078
  // deno-lint-ignore no-explicit-any
1027
1079
  let values: Record<string, any[]> & { "@id"?: string };
1028
1080
  let expanded: unknown[];
1029
- if (globalThis.Object.keys(json).length == 0) {
1081
+ if (
1082
+ "_fromSubclass" in options &&
1083
+ options._fromSubclass &&
1084
+ typeof json === "object" &&
1085
+ !Array.isArray(json)
1086
+ ) {
1087
+ values =
1088
+ // deno-lint-ignore no-explicit-any
1089
+ json as (Record<string, any[]> & { "@id"?: string });
1090
+ expanded = [values];
1091
+ } else if (globalThis.Object.keys(json).length == 0) {
1030
1092
  values = {};
1031
1093
  expanded = [values];
1032
1094
  } else {
@@ -1083,8 +1145,9 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1083
1145
  );
1084
1146
  `;
1085
1147
  else yield `
1086
- delete values["@type"];
1087
- const instance = await super.fromJsonLd(values, {
1148
+ const superValues = structuredClone(values);
1149
+ delete superValues["@type"];
1150
+ const instance = await super.fromJsonLd(superValues, {
1088
1151
  ...options,
1089
1152
  // @ts-ignore: an internal option
1090
1153
  _fromSubclass: true,
@@ -1093,25 +1156,35 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1093
1156
  throw new TypeError("Unexpected type: " + instance.constructor.name);
1094
1157
  }
1095
1158
  `;
1159
+ yield `
1160
+ let shouldCacheJsonLd = instance._shouldCacheJsonLd;
1161
+ `;
1096
1162
  for (const property of type.properties) {
1097
1163
  const variable = await getFieldName(property.uri, "");
1098
1164
  yield await generateField(property, types, "const ");
1099
1165
  const arrayVariable = `${variable}__array`;
1100
1166
  yield `
1101
- let ${arrayVariable} = values[${JSON.stringify(property.uri)}];
1167
+ let ${arrayVariable} = ${property.uri === "https://w3id.org/fep/ef61/gateways" ? "getPortableActorGateways(values) as typeof values[string]" : `values[${JSON.stringify(property.uri)}]`};
1102
1168
  `;
1103
1169
  if (property.functional && property.redundantProperties != null) for (const prop of property.redundantProperties) yield `
1104
1170
  if (${arrayVariable} == null || ${arrayVariable}.length < 1) {
1105
1171
  ${arrayVariable} = values[${JSON.stringify(prop.uri)}];
1106
1172
  }
1107
1173
  `;
1174
+ let itemsExpression = `${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1175
+ ? ${arrayVariable}[0]["@list"]
1176
+ : ${arrayVariable}`;
1177
+ if (isNonFunctionalProperty(property) && property.container === "graph") itemsExpression = `(${itemsExpression}).flatMap((item: unknown) =>
1178
+ item != null && typeof item === "object" && "@graph" in item &&
1179
+ Array.isArray((item as { "@graph": unknown })["@graph"])
1180
+ ? (item as { "@graph": unknown[] })["@graph"]
1181
+ : [item]
1182
+ )`;
1108
1183
  yield `
1109
1184
  for (
1110
1185
  const v of ${arrayVariable} == null
1111
1186
  ? []
1112
- : ${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1113
- ? ${arrayVariable}[0]["@list"]
1114
- : ${arrayVariable}
1187
+ : ${itemsExpression}
1115
1188
  ) {
1116
1189
  if (v == null) continue;
1117
1190
  `;
@@ -1125,29 +1198,47 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1125
1198
  continue;
1126
1199
  }
1127
1200
  `;
1128
- if (property.range.length == 1) yield `
1129
- const decoded = ${getDecoder(property.range[0], types, "v", "options", propertyBaseUrl)};
1130
- if (typeof decoded === "undefined") continue;
1131
- ${variable}.push(decoded);`;
1132
- else {
1133
- yield `
1201
+ yield `
1134
1202
  const decoded =
1135
- `;
1203
+ `;
1204
+ const lenient = property.range.length == 1 && skipsUnparsable(property.range[0]);
1205
+ if (property.range.length == 1) {
1206
+ if (lenient) {
1207
+ yield getDataCheck(property.range[0], types, "v");
1208
+ yield " ? ";
1209
+ }
1210
+ yield getDecoder(property.range[0], types, "v", "options", propertyBaseUrl);
1211
+ if (lenient) yield " : undefined";
1212
+ } else {
1136
1213
  const decoders = getDecoders(property.range, types, "v", "options", propertyBaseUrl);
1137
1214
  for (const code of decoders) yield code;
1138
- yield `
1215
+ }
1216
+ yield `
1139
1217
  ;
1140
- if (typeof decoded === "undefined") continue;
1218
+ `;
1219
+ yield `
1220
+ if (typeof decoded === "undefined") {
1221
+ shouldCacheJsonLd = false;
1222
+ continue;
1223
+ }
1224
+ `;
1225
+ yield `
1226
+ if (!this._shouldCacheDecodedJsonLd(decoded)) {
1227
+ shouldCacheJsonLd = false;
1228
+ }
1141
1229
  ${variable}.push(decoded);
1142
- `;
1143
- }
1230
+ `;
1144
1231
  yield `
1145
1232
  }
1146
1233
  instance.${await getFieldName(property.uri)} = ${variable};
1147
1234
  `;
1148
1235
  }
1149
1236
  yield `
1150
- if (!("_fromSubclass" in options) || !options._fromSubclass) {
1237
+ instance._shouldCacheJsonLd = shouldCacheJsonLd;
1238
+ if (
1239
+ shouldCacheJsonLd &&
1240
+ (!("_fromSubclass" in options) || !options._fromSubclass)
1241
+ ) {
1151
1242
  try {
1152
1243
  if (cacheJsonLd != null && cacheJsonLd !== expanded) {
1153
1244
  const compactArray = Array.isArray(json) && json.length === 1;
@@ -1213,6 +1304,7 @@ async function* generateConstructor(typeUri, types) {
1213
1304
  documentLoader?: DocumentLoader,
1214
1305
  contextLoader?: DocumentLoader,
1215
1306
  tracerProvider?: TracerProvider,
1307
+ verifyPortableObject?: PortableObjectVerifier,
1216
1308
  } = {},
1217
1309
  ) {
1218
1310
  `;
@@ -1220,6 +1312,7 @@ async function* generateConstructor(typeUri, types) {
1220
1312
  this.#documentLoader = options.documentLoader;
1221
1313
  this.#contextLoader = options.contextLoader;
1222
1314
  this.#tracerProvider = options.tracerProvider;
1315
+ this.#verifyPortableObject = options.verifyPortableObject;
1223
1316
  const baseUrl = (options as { baseUrl?: URL }).baseUrl;
1224
1317
  this.#_baseUrl = baseUrl == null ? undefined : new URL(baseUrl.href);
1225
1318
  if ("$warning" in options) {
@@ -1289,7 +1382,7 @@ async function* generateConstructor(typeUri, types) {
1289
1382
  if (Array.isArray(values.${property.pluralName}) &&
1290
1383
  values.${property.pluralName}.every(v => ${typeGuards})) {
1291
1384
  // @ts-ignore: type is checked above.
1292
- this.${fieldName} = values.${property.pluralName};
1385
+ this.${fieldName} = values.${property.pluralName}.slice();
1293
1386
  `;
1294
1387
  if (!allScalarTypes) yield `
1295
1388
  for (let i = 0; i < values.${property.pluralName}.length; i++) {
@@ -1314,9 +1407,17 @@ async function* generateCloner(typeUri, types) {
1314
1407
  yield `
1315
1408
  /**
1316
1409
  * Clones this instance, optionally updating it with the given values.
1410
+ *
1411
+ * A clone never inherits a signed JSON-LD representation retained by
1412
+ * \`signObject()\`: the clone may differ from the document that the proof
1413
+ * covers, so embedding the original secured JSON in a parent document
1414
+ * would be wrong. Sign the clone again if it has to be embedded as a
1415
+ * secured child.
1416
+ *
1317
1417
  * @param values The values to update the clone with.
1318
1418
  * @param options The options to use for cloning.
1319
- * @returns The cloned instance.
1419
+ * @returns The cloned instance with its own property arrays. Nested objects
1420
+ * and URLs are shared with this instance.
1320
1421
  */
1321
1422
  ${emitOverride$1(typeUri, types)} clone(
1322
1423
  values:
@@ -1327,6 +1428,7 @@ async function* generateCloner(typeUri, types) {
1327
1428
  options: {
1328
1429
  documentLoader?: DocumentLoader,
1329
1430
  contextLoader?: DocumentLoader,
1431
+ verifyPortableObject?: PortableObjectVerifier,
1330
1432
  } = {}
1331
1433
  ): ${type.name} {
1332
1434
  if (this._warning != null) {
@@ -1344,13 +1446,14 @@ async function* generateCloner(typeUri, types) {
1344
1446
  { id: values.id ?? this.id },
1345
1447
  options
1346
1448
  );
1449
+ copyPortableProvenance(this, clone);
1347
1450
  `;
1348
1451
  else yield `const clone = super.clone(values, options) as unknown as ${type.name};`;
1349
1452
  for (const property of type.properties) {
1350
1453
  const fieldName = await getFieldName(property.uri);
1351
1454
  const trustFieldName = await getFieldName(property.uri, "#_trust");
1352
1455
  const allScalarTypes = areAllScalarTypes(property.range, types);
1353
- yield `clone.${fieldName} = this.${fieldName};`;
1456
+ yield `clone.${fieldName} = this.${fieldName}.slice();`;
1354
1457
  if (!allScalarTypes) yield `clone.${trustFieldName} = new Set(this.${trustFieldName});`;
1355
1458
  if (hasSingularAccessor(property)) {
1356
1459
  let typeGuards = getTypeGuards(property.range, types, `values.${property.singularName}`);
@@ -1401,7 +1504,7 @@ async function* generateCloner(typeUri, types) {
1401
1504
  if (Array.isArray(values.${property.pluralName}) &&
1402
1505
  values.${property.pluralName}.every(v => ${typeGuards})) {
1403
1506
  // @ts-ignore: type is checked above.
1404
- clone.${fieldName} = values.${property.pluralName};
1507
+ clone.${fieldName} = values.${property.pluralName}.slice();
1405
1508
  `;
1406
1509
  if (!allScalarTypes) yield `
1407
1510
  clone.${trustFieldName} = new Set();
@@ -1439,12 +1542,12 @@ async function* generateInspector(typeUri, types) {
1439
1542
  [Symbol.for("Deno.customInspect")]: (
1440
1543
  inspect: typeof Deno.inspect,
1441
1544
  options: Deno.InspectOptions,
1442
- ): string => "URL " + inspect(this.id!.href, options),
1545
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
1443
1546
  [Symbol.for("nodejs.util.inspect.custom")]: (
1444
1547
  _depth: number,
1445
1548
  options: unknown,
1446
1549
  inspect: (value: unknown, options: unknown) => string,
1447
- ): string => "URL " + inspect(this.id!.href, options),
1550
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
1448
1551
  };
1449
1552
  }
1450
1553
  `;
@@ -1460,12 +1563,12 @@ async function* generateInspector(typeUri, types) {
1460
1563
  [Symbol.for("Deno.customInspect")]: (
1461
1564
  inspect: typeof Deno.inspect,
1462
1565
  options: Deno.InspectOptions,
1463
- ): string => "URL " + inspect(v.href, options),
1566
+ ): string => "URL " + inspect(inspectIri(v), options),
1464
1567
  [Symbol.for("nodejs.util.inspect.custom")]: (
1465
1568
  _depth: number,
1466
1569
  options: unknown,
1467
1570
  inspect: (value: unknown, options: unknown) => string,
1468
- ): string => "URL " + inspect(v.href, options),
1571
+ ): string => "URL " + inspect(inspectIri(v), options),
1469
1572
  }
1470
1573
  : v);
1471
1574
  `;
@@ -1535,6 +1638,7 @@ function emitOverride(typeUri, types, property) {
1535
1638
  }
1536
1639
  async function* generateProperty(type, property, types, moduleVarNames) {
1537
1640
  const override = emitOverride(type.uri, types, property);
1641
+ const trustOwner = type.trustEmbeddedObjects === false ? "null" : "this.id";
1538
1642
  const doc = `\n/** ${property.description.replaceAll("\n", "\n * ")}\n */\n`;
1539
1643
  const cachedPropertyBaseUrl = `(options as { baseUrl?: URL }).baseUrl ??
1540
1644
  this._baseUrl ??
@@ -1575,6 +1679,9 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1575
1679
  suppressError?: boolean,
1576
1680
  tracerProvider?: TracerProvider,
1577
1681
  crossOrigin?: "ignore" | "throw" | "trust";
1682
+ gateways?: readonly (string | URL)[];
1683
+ verifyPortableObject?: PortableObjectVerifier;
1684
+ inheritPortableObjectVerifier?: boolean;
1578
1685
  } = {},
1579
1686
  ): Promise<${getTypeNames(property.range, types)} | null> {
1580
1687
  const documentLoader =
@@ -1587,11 +1694,96 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1587
1694
  ${JSON.stringify(name)},
1588
1695
  ${JSON.stringify(version)},
1589
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;
1590
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
+ }
1591
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
+ }
1592
1782
  let fetchResult: RemoteDocument;
1593
1783
  try {
1594
- fetchResult = await documentLoader(lookupUrl);
1784
+ fetchResult = await documentLoader(lookupUrl, {
1785
+ suppressError: options.suppressError,
1786
+ });
1595
1787
  } catch (error) {
1596
1788
  span.setStatus({
1597
1789
  code: SpanStatusCode.ERROR,
@@ -1599,7 +1791,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1599
1791
  });
1600
1792
  span.end();
1601
1793
  if (options.suppressError) {
1602
- getLogger(["fedify", "vocab"]).error(
1794
+ getLogger(["fedify", "vocab"]).warn(
1603
1795
  "Failed to fetch {url}: {error}",
1604
1796
  { error, url: lookupUrl }
1605
1797
  );
@@ -1608,52 +1800,96 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1608
1800
  throw error;
1609
1801
  }
1610
1802
  const { document, documentUrl } = fetchResult;
1611
- 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
+ );
1612
1812
  try {
1613
- const obj = await this.#${property.singularName}_fromJsonLd(
1614
- document,
1615
- { documentLoader, contextLoader, tracerProvider, baseUrl }
1616
- );
1617
- if (obj?.id != null && !isTrustedIriOrigin(options, obj.id, baseUrl)) {
1618
- if (options.crossOrigin === "throw") {
1619
- throw new Error(
1620
- "The object's @id (" + obj.id.href + ") has a different origin " +
1621
- "than the document URL (" + baseUrl.href + "); refusing to return " +
1622
- "the object. If you want to bypass this check and are aware of" +
1623
- 'the security implications, set the crossOrigin option to "trust".'
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
1624
1859
  );
1860
+ span.end();
1861
+ return obj;
1625
1862
  }
1626
- getLogger(["fedify", "vocab"]).warn(
1627
- "The object's @id ({objectId}) has a different origin than the document " +
1628
- "URL ({documentUrl}); refusing to return the object. If you want to " +
1629
- "bypass this check and are aware of the security implications, " +
1630
- 'set the crossOrigin option to "trust".',
1631
- { ...fetchResult, objectId: obj.id.href },
1632
- );
1633
- return null;
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;
1634
1878
  }
1635
- span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
1636
- span.setAttribute(
1637
- "activitypub.object.type",
1638
- // @ts-ignore: obj.constructor always has a typeId.
1639
- obj.constructor.typeId.href
1640
- );
1641
- return obj;
1642
- } catch (e) {
1643
- if (options.suppressError) {
1644
- getLogger(["fedify", "vocab"]).error(
1645
- "Failed to parse {url}: {error}",
1646
- { error: e, url: lookupUrl }
1647
- );
1648
- return null;
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);
1649
1884
  }
1650
- span.setStatus({
1651
- code: SpanStatusCode.ERROR,
1652
- message: String(e),
1885
+ return await dereferencePortable(claim.id, {
1886
+ inferredGateways: claim.inferredGateways,
1887
+ response: fetchResult,
1888
+ contextLoader: snapshot?.loader,
1653
1889
  });
1654
- throw e;
1655
1890
  } finally {
1656
- span.end();
1891
+ scopedContext.release();
1892
+ snapshot?.release();
1657
1893
  }
1658
1894
  });
1659
1895
  }
@@ -1664,6 +1900,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1664
1900
  documentLoader?: DocumentLoader,
1665
1901
  contextLoader?: DocumentLoader,
1666
1902
  tracerProvider?: TracerProvider,
1903
+ verifyPortableObject?: PortableObjectVerifier,
1667
1904
  baseUrl?: URL
1668
1905
  }
1669
1906
  ): Promise<${getTypeNames(property.range, types)}> {
@@ -1674,6 +1911,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1674
1911
  const tracerProvider = options.tracerProvider ??
1675
1912
  this._tracerProvider ?? trace.getTracerProvider();
1676
1913
  const baseUrl = options.baseUrl;
1914
+ const verifyPortableObject = options.verifyPortableObject;
1677
1915
  `;
1678
1916
  if (property.preprocessors != null && property.preprocessors.length > 0) {
1679
1917
  yield `
@@ -1693,6 +1931,7 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1693
1931
  documentLoader,
1694
1932
  contextLoader,
1695
1933
  tracerProvider,
1934
+ verifyPortableObject,
1696
1935
  baseUrl,
1697
1936
  });
1698
1937
  if (_result instanceof Error) throw _result;
@@ -1711,7 +1950,13 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1711
1950
  try {
1712
1951
  return await ${types[range].name}.fromJsonLd(
1713
1952
  jsonLd,
1714
- { documentLoader, contextLoader, tracerProvider, baseUrl },
1953
+ {
1954
+ documentLoader,
1955
+ contextLoader,
1956
+ tracerProvider,
1957
+ verifyPortableObject,
1958
+ baseUrl,
1959
+ },
1715
1960
  );
1716
1961
  } catch (e) {
1717
1962
  if (!(e instanceof TypeError)) throw e;
@@ -1753,6 +1998,21 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1753
1998
  suppressError?: boolean,
1754
1999
  tracerProvider?: TracerProvider,
1755
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;
1756
2016
  } = {}
1757
2017
  ): Promise<${getTypeNames(property.range, types)} | null> {
1758
2018
  if (this._warning != null) {
@@ -1762,25 +2022,46 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1762
2022
  );
1763
2023
  }
1764
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
+ }
1765
2034
  let v = this.${await getFieldName(property.uri)}[0];
1766
2035
  if (!(v instanceof URL) &&
1767
- v.id != null &&
1768
- !isTrustedIriOrigin(options, v.id, this.id) &&
1769
2036
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
1770
- 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
+ }
1771
2049
  }
1772
2050
  if (v instanceof URL) {
1773
2051
  const fetched =
1774
2052
  await this.#fetch${pascalCase(property.singularName)}(v, options);
1775
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;
1776
2057
  this.${await getFieldName(property.uri)}[0] = fetched;
1777
2058
  this.${await getFieldName(property.uri, "#_trust")}.add(0);
1778
2059
  this._cachedJsonLd = undefined;
2060
+ recordPortableReferrer(this, fetched, ${JSON.stringify(property.uri)});
1779
2061
  return fetched;
1780
2062
  }
1781
2063
  `;
1782
- if (property.compactName != null) {
1783
- yield `
2064
+ if (property.compactName != null) yield `
1784
2065
  if (
1785
2066
  this._cachedJsonLd != null &&
1786
2067
  typeof this._cachedJsonLd === "object" &&
@@ -1791,26 +2072,36 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1791
2072
  ${JSON.stringify(property.compactName)}];
1792
2073
  const doc = Array.isArray(prop) ? prop[0] : prop;
1793
2074
  if (doc != null && typeof doc === "object" && "@context" in doc) {
1794
- `;
1795
- yield `
1796
- v = await this.#${property.singularName}_fromJsonLd(doc, {
1797
- ...options,
1798
- baseUrl: ${cachedPropertyBaseUrl},
1799
- });
1800
- `;
1801
- yield `
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
+ }
1802
2090
  }
1803
2091
  }
1804
2092
  `;
1805
- }
1806
2093
  yield `
1807
2094
  if (v?.id != null &&
1808
- 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
+ )) &&
1809
2100
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
1810
2101
  if (options.crossOrigin === "throw") {
1811
2102
  throw new Error(
1812
2103
  "The property object's @id (" + v.id.href + ") has a different " +
1813
- "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)"} + "); " +
1814
2105
  "refusing to return the object. If you want to bypass this " +
1815
2106
  "check and are aware of the security implications, set the " +
1816
2107
  'crossOrigin option to "trust".'
@@ -1822,10 +2113,11 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1822
2113
  "return the object. If you want to bypass this check and are " +
1823
2114
  "aware of the security implications, set the crossOrigin option " +
1824
2115
  'to "trust".',
1825
- { objectId: v.id.href, parentObjectId: this.id.href },
2116
+ { objectId: v.id.href, parentObjectId: ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} },
1826
2117
  );
1827
2118
  return null;
1828
2119
  }
2120
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
1829
2121
  return v;
1830
2122
  }
1831
2123
  `;
@@ -1858,6 +2150,21 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1858
2150
  suppressError?: boolean,
1859
2151
  tracerProvider?: TracerProvider,
1860
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;
1861
2168
  } = {}
1862
2169
  ): AsyncIterable<${getTypeNames(property.range, types)}> {
1863
2170
  if (this._warning != null) {
@@ -1866,28 +2173,52 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1866
2173
  this._warning.values
1867
2174
  );
1868
2175
  }
2176
+ if (
2177
+ options.verifyPortableObject == null &&
2178
+ this._verifyPortableObject != null
2179
+ ) {
2180
+ options = {
2181
+ ...options,
2182
+ verifyPortableObject: this._verifyPortableObject,
2183
+ };
2184
+ }
1869
2185
  const vs = this.${await getFieldName(property.uri)};
1870
2186
  for (let i = 0; i < vs.length; i++) {
1871
2187
  let v = vs[i];
1872
2188
  if (!(v instanceof URL) &&
1873
- v.id != null &&
1874
- !isTrustedIriOrigin(options, v.id, this.id) &&
1875
2189
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
1876
- 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
+ }
1877
2202
  }
1878
2203
  if (v instanceof URL) {
1879
2204
  const fetched =
1880
2205
  await this.#fetch${pascalCase(property.singularName)}(v, options);
1881
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
+ }
1882
2213
  vs[i] = fetched;
1883
2214
  this.${await getFieldName(property.uri, "#_trust")}.add(i);
1884
2215
  this._cachedJsonLd = undefined;
2216
+ recordPortableReferrer(this, fetched, ${JSON.stringify(property.uri)});
1885
2217
  yield fetched;
1886
2218
  continue;
1887
2219
  }
1888
2220
  `;
1889
- if (property.compactName != null) {
1890
- yield `
2221
+ if (property.compactName != null) yield `
1891
2222
  if (
1892
2223
  this._cachedJsonLd != null &&
1893
2224
  typeof this._cachedJsonLd === "object" &&
@@ -1898,26 +2229,36 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1898
2229
  ${JSON.stringify(property.compactName)}];
1899
2230
  const obj = Array.isArray(prop) ? prop[i] : prop;
1900
2231
  if (obj != null && typeof obj === "object" && "@context" in obj) {
1901
- `;
1902
- yield `
1903
- v = await this.#${property.singularName}_fromJsonLd(obj, {
1904
- ...options,
1905
- baseUrl: ${cachedPropertyBaseUrl},
1906
- });
1907
- `;
1908
- yield `
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
+ }
1909
2247
  }
1910
2248
  }
1911
2249
  `;
1912
- }
1913
2250
  yield `
1914
2251
  if (v?.id != null &&
1915
- 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
+ )) &&
1916
2257
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
1917
2258
  if (options.crossOrigin === "throw") {
1918
2259
  throw new Error(
1919
2260
  "The property object's @id (" + v.id.href + ") has a different " +
1920
- "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)"} + "); " +
1921
2262
  "refusing to return the object. If you want to bypass this " +
1922
2263
  "check and are aware of the security implications, set the " +
1923
2264
  'crossOrigin option to "trust".'
@@ -1929,10 +2270,11 @@ async function* generateProperty(type, property, types, moduleVarNames) {
1929
2270
  "return the object. If you want to bypass this check and are " +
1930
2271
  "aware of the security implications, set the crossOrigin " +
1931
2272
  'option to "trust".',
1932
- { objectId: v.id.href, parentObjectId: this.id.href },
2273
+ { objectId: v.id.href, parentObjectId: ${type.trustEmbeddedObjects === false ? "\"untrusted metadata\"" : "(this.id?.href ?? null)"} },
1933
2274
  );
1934
2275
  continue;
1935
2276
  }
2277
+ recordPortableReferrer(this, v, ${JSON.stringify(property.uri)});
1936
2278
  yield v;
1937
2279
  }
1938
2280
  }
@@ -1950,10 +2292,35 @@ const XSD_ANY_URI = "http://www.w3.org/2001/XMLSchema#anyURI";
1950
2292
  const FEDIFY_URL = "fedify:url";
1951
2293
  const INTERNAL_RUNTIME_IMPORTS = [
1952
2294
  "compactJsonLdCache",
2295
+ "createScopedContextLoader",
1953
2296
  "getJsonLdContext",
2297
+ "getPortableActorGateways",
1954
2298
  "isTrustedIriOrigin",
1955
2299
  "normalizeJsonLdIris"
1956
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 ");
1957
2324
  const RUNTIME_IMPORTS = [
1958
2325
  "canParseDecimal",
1959
2326
  "decodeMultibase",
@@ -1969,10 +2336,12 @@ const RUNTIME_IMPORTS = [
1969
2336
  "isDecimal",
1970
2337
  "isGatewayUrl",
1971
2338
  "LanguageString",
2339
+ "normalizeLanguageTag",
1972
2340
  "parseDecimal",
1973
2341
  "parseGatewayUrl",
1974
2342
  "parseIri",
1975
2343
  "parseJsonLdId",
2344
+ "type PortableObjectVerifier",
1976
2345
  "type RemoteDocument"
1977
2346
  ].join(",\n ");
1978
2347
  /**
@@ -2008,6 +2377,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2008
2377
  readonly #documentLoader?: DocumentLoader;
2009
2378
  readonly #contextLoader?: DocumentLoader;
2010
2379
  readonly #tracerProvider?: TracerProvider;
2380
+ readonly #verifyPortableObject?: PortableObjectVerifier;
2011
2381
  readonly #warning?: {
2012
2382
  category: string[];
2013
2383
  message: string;
@@ -2015,6 +2385,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2015
2385
  };
2016
2386
  #cachedJsonLd?: unknown;
2017
2387
  readonly #_baseUrl?: URL;
2388
+ #shouldCacheJsonLd = true;
2018
2389
  readonly id: URL | null;
2019
2390
 
2020
2391
  protected get _documentLoader(): DocumentLoader | undefined {
@@ -2029,6 +2400,10 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2029
2400
  return this.#tracerProvider;
2030
2401
  }
2031
2402
 
2403
+ protected get _verifyPortableObject(): PortableObjectVerifier | undefined {
2404
+ return this.#verifyPortableObject;
2405
+ }
2406
+
2032
2407
  protected get _warning(): {
2033
2408
  category: string[];
2034
2409
  message: string;
@@ -2048,6 +2423,20 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2048
2423
  protected get _baseUrl(): URL | undefined {
2049
2424
  return this.#_baseUrl;
2050
2425
  }
2426
+
2427
+ protected get _shouldCacheJsonLd(): boolean {
2428
+ return this.#shouldCacheJsonLd;
2429
+ }
2430
+
2431
+ protected set _shouldCacheJsonLd(value: boolean) {
2432
+ this.#shouldCacheJsonLd = value;
2433
+ }
2434
+
2435
+ protected static _shouldCacheDecodedJsonLd(value: unknown): boolean {
2436
+ if (value == null || typeof value !== "object") return true;
2437
+ if (!("_shouldCacheJsonLd" in value)) return true;
2438
+ return (value as { _shouldCacheJsonLd: boolean })._shouldCacheJsonLd;
2439
+ }
2051
2440
  `;
2052
2441
  yield `
2053
2442
  /**
@@ -2134,11 +2523,69 @@ async function* generateClasses(types) {
2134
2523
  from "@opentelemetry/api";\n`;
2135
2524
  yield `import {\n ${RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime";\n`;
2136
2525
  yield `import {\n ${INTERNAL_RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime/internal/jsonld-cache";\n`;
2526
+ yield `import {\n ${PORTABLE_DEREFERENCE_IMPORTS}\n} from "@fedify/vocab-runtime/internal/portable-dereference";\n`;
2527
+ yield `import {\n ${SIGNED_REPRESENTATION_IMPORTS}\n} from "@fedify/vocab-runtime/internal/signed-representation";\n`;
2137
2528
  yield `import {
2138
2529
  isTemporalDuration,
2139
2530
  isTemporalInstant,
2140
2531
  } from "@fedify/vocab-runtime/temporal";\n`;
2141
- const portableIriKeys = new Set(["@id", "id"]);
2532
+ yield `
2533
+ function canDecodeIri(iri: string): boolean {
2534
+ try { parseIri(iri); return true; } catch { return iri.startsWith("at://"); }
2535
+ }
2536
+
2537
+ function decodeIri(iri: string): URL {
2538
+ return !URL.canParse(iri) && iri.startsWith("at://")
2539
+ ? new URL("at://" + encodeURIComponent(iri.substring(5)))
2540
+ : parseIri(iri);
2541
+ }
2542
+
2543
+ function inspectIri(iri: URL): string {
2544
+ // Inspecting an object must not throw, even for a URL that cannot be
2545
+ // formatted as an IRI, e.g., a malformed portable ID:
2546
+ try { return formatIri(iri); } catch { return iri.href; }
2547
+ }
2548
+ `;
2549
+ yield `
2550
+ function isValidLanguageTag(language: string): boolean {
2551
+ try {
2552
+ new Intl.Locale(normalizeLanguageTag(language));
2553
+ return true;
2554
+ } catch (error) {
2555
+ if (error instanceof RangeError) return false;
2556
+ throw error;
2557
+ }
2558
+ }
2559
+ `;
2560
+ const extraContexts = Object.fromEntries(Object.values(types).flatMap((type) => type.properties.flatMap((property) => property.extraContext == null ? [] : [[property.uri, property.extraContext]])));
2561
+ if (Object.keys(extraContexts).length > 0) yield `
2562
+ const extraPropertyContexts: Readonly<Record<string, string>> =
2563
+ ${JSON.stringify(extraContexts)};
2564
+
2565
+ function getExtraContexts(document: unknown): string[] {
2566
+ const contexts = new Set<string>();
2567
+ const pending: unknown[] = [document];
2568
+ while (pending.length > 0) {
2569
+ const value = pending.pop();
2570
+ if (value == null || typeof value !== "object") continue;
2571
+ if (Array.isArray(value)) {
2572
+ for (const item of value) pending.push(item);
2573
+ continue;
2574
+ }
2575
+ if ("@value" in value) continue;
2576
+ for (const [key, child] of globalThis.Object.entries(value)) {
2577
+ if (key === "@context") continue;
2578
+ if (globalThis.Object.hasOwn(extraPropertyContexts, key)) {
2579
+ contexts.add(extraPropertyContexts[key]);
2580
+ }
2581
+ pending.push(child);
2582
+ }
2583
+ }
2584
+ return [...contexts];
2585
+ }
2586
+ `;
2587
+ yield "\n\n";
2588
+ const portableIriKeys = /* @__PURE__ */ new Set(["@id", "id"]);
2142
2589
  for (const type of Object.values(types)) for (const property of type.properties) addPortableIriKeys(portableIriKeys, property, types);
2143
2590
  yield "const PORTABLE_IRI_PATTERN = /^ap(?:\\+ef61)?:\\/\\//i;\n";
2144
2591
  yield `const PORTABLE_IRI_KEYS: ReadonlySet<string> = new Set(${JSON.stringify([...portableIriKeys].sort())});\n\n`;