@fedify/vocab 2.4.0-dev.2122 → 2.4.0-dev.2129

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.
@@ -15,12 +15,13 @@ import {
15
15
  } from "@fedify/vocab-runtime";
16
16
  import fetchMock from "fetch-mock";
17
17
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
18
- import { traverseCollection } from "./lookup.ts";
18
+ import { lookupObject, traverseCollection } from "./lookup.ts";
19
19
  import { assertInstanceOf } from "./utils.ts";
20
20
  import {
21
21
  Collection,
22
22
  CollectionPage,
23
23
  Create,
24
+ Image,
24
25
  Note,
25
26
  OrderedCollection,
26
27
  Person,
@@ -1858,3 +1859,336 @@ test("singular accessors drop anonymous objects embedded in unsecured collection
1858
1859
  [],
1859
1860
  );
1860
1861
  });
1862
+
1863
+ function createCompatibleActivity(
1864
+ object: unknown,
1865
+ id: string = gatewayUrl("https://gw.example", "/activities/1"),
1866
+ options: { verifyPortableObject?: PortableObjectVerifier } = {},
1867
+ ): Promise<Create> {
1868
+ return Create.fromJsonLd({
1869
+ "@context": "https://www.w3.org/ns/activitystreams",
1870
+ type: "Create",
1871
+ id,
1872
+ object,
1873
+ }, { contextLoader: mockDocumentLoader, ...options });
1874
+ }
1875
+
1876
+ test("accessors of objects with compatible identifiers dereference references as portable objects", async (t) => {
1877
+ await t.step("verifying the object at the reference", async () => {
1878
+ const documentLoader = createLoader({
1879
+ [compatibleNoteId]: { ...note(), id: compatibleNoteId },
1880
+ });
1881
+ const verifier = createRecordingVerifier();
1882
+ const activity = await createCompatibleActivity(compatibleNoteId);
1883
+ const object = await activity.getObject({
1884
+ documentLoader,
1885
+ contextLoader: mockDocumentLoader,
1886
+ verifyPortableObject: verifier,
1887
+ });
1888
+ assertInstanceOf(object, Note);
1889
+ deepStrictEqual(object.id, new URL(compatibleNoteId));
1890
+ deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
1891
+ deepStrictEqual(verifier.calls.length, 1);
1892
+ deepStrictEqual(verifier.calls[0].options.gatewayHints, [
1893
+ new URL("https://gw.example"),
1894
+ ]);
1895
+ ok(verifier.calls[0].options.referrer?.object === activity);
1896
+ // The root itself is not marked as verified:
1897
+ deepStrictEqual(verifier.calls[0].options.referrer?.acceptance, undefined);
1898
+ });
1899
+
1900
+ await t.step("rejecting an unsigned object at the reference", async () => {
1901
+ const documentLoader = createLoader({
1902
+ [compatibleNoteId]: { ...note(), id: compatibleNoteId },
1903
+ });
1904
+ const activity = await createCompatibleActivity(compatibleNoteId);
1905
+ deepStrictEqual(
1906
+ await activity.getObject({
1907
+ documentLoader,
1908
+ contextLoader: mockDocumentLoader,
1909
+ verifyPortableObject: createVerifier(false),
1910
+ }),
1911
+ null,
1912
+ );
1913
+ // Even with crossOrigin: "trust":
1914
+ deepStrictEqual(
1915
+ await Array.fromAsync(
1916
+ (await createCompatibleActivity([compatibleNoteId])).getObjects({
1917
+ documentLoader,
1918
+ contextLoader: mockDocumentLoader,
1919
+ verifyPortableObject: createVerifier(false),
1920
+ crossOrigin: "trust",
1921
+ }),
1922
+ ),
1923
+ [],
1924
+ );
1925
+ });
1926
+
1927
+ await t.step("without verifyPortableObject", async () => {
1928
+ const documentLoader = createLoader({
1929
+ [compatibleNoteId]: { ...note(), id: compatibleNoteId },
1930
+ });
1931
+ const activity = await createCompatibleActivity(compatibleNoteId);
1932
+ await rejects(
1933
+ async () =>
1934
+ await activity.getObject({
1935
+ documentLoader,
1936
+ contextLoader: mockDocumentLoader,
1937
+ }),
1938
+ TypeError,
1939
+ );
1940
+ deepStrictEqual(
1941
+ await activity.getObject({
1942
+ documentLoader,
1943
+ contextLoader: mockDocumentLoader,
1944
+ suppressError: true,
1945
+ }),
1946
+ null,
1947
+ );
1948
+ deepStrictEqual(documentLoader.fetched, []);
1949
+ });
1950
+
1951
+ await t.step("embedded objects with compatible identifiers", async () => {
1952
+ // Even on the same gateway and with the same DID, an embedded object
1953
+ // with a compatible identifier is verified on its own:
1954
+ const documentLoader = createLoader({ [compatibleNoteId]: note() });
1955
+ const verifier = createRecordingVerifier();
1956
+ const activity = await createCompatibleActivity({
1957
+ type: "Note",
1958
+ id: compatibleNoteId,
1959
+ content: "Embedded",
1960
+ });
1961
+ const object = await activity.getObject({
1962
+ documentLoader,
1963
+ contextLoader: mockDocumentLoader,
1964
+ verifyPortableObject: verifier,
1965
+ });
1966
+ assertInstanceOf(object, Note);
1967
+ deepStrictEqual(object.content, "Portable note");
1968
+ deepStrictEqual(verifier.calls.length, 1);
1969
+ });
1970
+
1971
+ await t.step("objects with malformed compatible identifiers", async () => {
1972
+ const malformedId = `${
1973
+ gatewayUrl("https://gw.example", "/activities/1")
1974
+ }?@gateway=https%3A%2F%2Fgw2.example`;
1975
+ // Portable references need a verifier:
1976
+ const activity = await createCompatibleActivity(objectId, malformedId);
1977
+ await rejects(
1978
+ async () =>
1979
+ await activity.getObject({
1980
+ documentLoader: createLoader({}),
1981
+ contextLoader: mockDocumentLoader,
1982
+ }),
1983
+ TypeError,
1984
+ );
1985
+ // Ordinary HTTP(S) references are still fetched without one:
1986
+ const plainUrl = "https://example.com/notes/1";
1987
+ const plainLoader = createLoader({
1988
+ [plainUrl]: { ...note(), id: plainUrl },
1989
+ });
1990
+ const plain = await (await createCompatibleActivity(plainUrl, malformedId))
1991
+ .getObject({
1992
+ documentLoader: plainLoader,
1993
+ contextLoader: mockDocumentLoader,
1994
+ });
1995
+ deepStrictEqual(plain?.id, new URL(plainUrl));
1996
+ // ...unless the response turns out to be a portable object:
1997
+ const claimLoader = createLoader({ [plainUrl]: note() });
1998
+ await rejects(
1999
+ async () =>
2000
+ await (await createCompatibleActivity(plainUrl, malformedId))
2001
+ .getObject({
2002
+ documentLoader: claimLoader,
2003
+ contextLoader: mockDocumentLoader,
2004
+ }),
2005
+ TypeError,
2006
+ );
2007
+ });
2008
+
2009
+ await t.step("objects with ordinary HTTP(S) identifiers", async () => {
2010
+ const documentLoader = createLoader({
2011
+ [compatibleNoteId]: { ...note(), id: compatibleNoteId },
2012
+ });
2013
+ const object = await (await createHttpActivity(compatibleNoteId))
2014
+ .getObject({ documentLoader, contextLoader: mockDocumentLoader });
2015
+ deepStrictEqual(object?.id, new URL(compatibleNoteId));
2016
+ });
2017
+ });
2018
+
2019
+ test("objects use the verifyPortableObject option they were parsed with by default", async (t) => {
2020
+ const attributedNote = (id: string) => ({
2021
+ ...note(id),
2022
+ attributedTo: `ap://${did}/actor`,
2023
+ });
2024
+ const actor = {
2025
+ "@context": "https://www.w3.org/ns/activitystreams",
2026
+ id: `ap://${did}/actor`,
2027
+ type: "Person",
2028
+ inbox: `ap://${did}/actor/inbox`,
2029
+ };
2030
+ const documentLoader = createLoader({
2031
+ [compatibleNoteId]: attributedNote(compatibleNoteId),
2032
+ [gatewayUrl("https://gw.example", "/actor")]: actor,
2033
+ });
2034
+
2035
+ await t.step("fromJsonLd()", async () => {
2036
+ const verifier = createRecordingVerifier();
2037
+ const activity = await createCompatibleActivity(
2038
+ compatibleNoteId,
2039
+ undefined,
2040
+ { verifyPortableObject: verifier },
2041
+ );
2042
+ const object = await activity.getObject({
2043
+ documentLoader,
2044
+ contextLoader: mockDocumentLoader,
2045
+ });
2046
+ assertInstanceOf(object, Note);
2047
+ deepStrictEqual(verifier.calls.length, 1);
2048
+ // Fetched objects inherit the default:
2049
+ const author = await object.getAttribution({
2050
+ documentLoader,
2051
+ contextLoader: mockDocumentLoader,
2052
+ gateways: ["https://gw.example"],
2053
+ });
2054
+ assertInstanceOf(author, Person);
2055
+ deepStrictEqual(verifier.calls.length, 2);
2056
+ // Embedded objects too:
2057
+ const embedding = await createCompatibleActivity(
2058
+ attributedNote(`ap://${did}/objects/2`),
2059
+ `ap://${did}/activities/1`,
2060
+ { verifyPortableObject: verifier },
2061
+ );
2062
+ const embedded = await embedding.getObject({
2063
+ documentLoader,
2064
+ contextLoader: mockDocumentLoader,
2065
+ });
2066
+ assertInstanceOf(embedded, Note);
2067
+ assertInstanceOf(
2068
+ await embedded.getAttribution({
2069
+ documentLoader,
2070
+ contextLoader: mockDocumentLoader,
2071
+ gateways: ["https://gw.example"],
2072
+ }),
2073
+ Person,
2074
+ );
2075
+ deepStrictEqual(verifier.calls.length, 3);
2076
+ });
2077
+
2078
+ await t.step("the constructor and clone()", async () => {
2079
+ const verifier = createRecordingVerifier();
2080
+ const activity = new Create({
2081
+ id: new URL(gatewayUrl("https://gw.example", "/activities/1")),
2082
+ object: new URL(compatibleNoteId),
2083
+ }, { contextLoader: mockDocumentLoader, verifyPortableObject: verifier });
2084
+ assertInstanceOf(
2085
+ await activity.getObject({
2086
+ documentLoader,
2087
+ contextLoader: mockDocumentLoader,
2088
+ }),
2089
+ Note,
2090
+ );
2091
+ deepStrictEqual(verifier.calls.length, 1);
2092
+ // A clone does not inherit it, but takes it as an option:
2093
+ const clone = activity.clone({ object: new URL(compatibleNoteId) });
2094
+ await rejects(
2095
+ async () =>
2096
+ await clone.getObject({
2097
+ documentLoader,
2098
+ contextLoader: mockDocumentLoader,
2099
+ }),
2100
+ TypeError,
2101
+ );
2102
+ const clone2 = activity.clone(
2103
+ { object: new URL(compatibleNoteId) },
2104
+ { contextLoader: mockDocumentLoader, verifyPortableObject: verifier },
2105
+ );
2106
+ assertInstanceOf(
2107
+ await clone2.getObject({
2108
+ documentLoader,
2109
+ contextLoader: mockDocumentLoader,
2110
+ }),
2111
+ Note,
2112
+ );
2113
+ deepStrictEqual(verifier.calls.length, 2);
2114
+ });
2115
+
2116
+ await t.step("an explicit option applies to that call only", async () => {
2117
+ const stored = createRecordingVerifier();
2118
+ const given = createRecordingVerifier();
2119
+ const activity = await createCompatibleActivity(
2120
+ compatibleNoteId,
2121
+ undefined,
2122
+ { verifyPortableObject: stored },
2123
+ );
2124
+ const object = await activity.getObject({
2125
+ documentLoader,
2126
+ contextLoader: mockDocumentLoader,
2127
+ verifyPortableObject: given,
2128
+ });
2129
+ assertInstanceOf(object, Note);
2130
+ deepStrictEqual([stored.calls.length, given.calls.length], [0, 1]);
2131
+ // The fetched object inherits the parent's default, not the verifier
2132
+ // given to the call:
2133
+ await object.getAttribution({
2134
+ documentLoader,
2135
+ contextLoader: mockDocumentLoader,
2136
+ gateways: ["https://gw.example"],
2137
+ });
2138
+ deepStrictEqual([stored.calls.length, given.calls.length], [1, 1]);
2139
+
2140
+ // Without a default, the fetched object has none either:
2141
+ const plain = await createCompatibleActivity(compatibleNoteId);
2142
+ const object2 = await plain.getObject({
2143
+ documentLoader,
2144
+ contextLoader: mockDocumentLoader,
2145
+ verifyPortableObject: given,
2146
+ });
2147
+ assertInstanceOf(object2, Note);
2148
+ await rejects(
2149
+ async () =>
2150
+ await object2.getAttribution({
2151
+ documentLoader,
2152
+ contextLoader: mockDocumentLoader,
2153
+ gateways: ["https://gw.example"],
2154
+ }),
2155
+ TypeError,
2156
+ );
2157
+ });
2158
+
2159
+ await t.step("lookupObject()", async () => {
2160
+ const verifier = createRecordingVerifier();
2161
+ const object = await lookupObject(compatibleNoteId, {
2162
+ documentLoader,
2163
+ contextLoader: mockDocumentLoader,
2164
+ verifyPortableObject: verifier,
2165
+ });
2166
+ assertInstanceOf(object, Note);
2167
+ deepStrictEqual(verifier.calls.length, 1);
2168
+ assertInstanceOf(
2169
+ await object.getAttribution({
2170
+ documentLoader,
2171
+ contextLoader: mockDocumentLoader,
2172
+ gateways: ["https://gw.example"],
2173
+ }),
2174
+ Person,
2175
+ );
2176
+ deepStrictEqual(verifier.calls.length, 2);
2177
+ });
2178
+
2179
+ await t.step("property preprocessors", async () => {
2180
+ const verifier = createVerifier();
2181
+ const person = await Person.fromJsonLd({
2182
+ "@context": "https://www.w3.org/ns/activitystreams",
2183
+ id: `ap://${did}/actor`,
2184
+ type: "Person",
2185
+ icon: { type: "Link", href: "https://example.com/icon.png" },
2186
+ }, { contextLoader: mockDocumentLoader, verifyPortableObject: verifier });
2187
+ const icon = await person.getIcon({ documentLoader });
2188
+ assertInstanceOf(icon, Image);
2189
+ ok(
2190
+ (icon as unknown as { _verifyPortableObject: unknown })
2191
+ ._verifyPortableObject === verifier,
2192
+ );
2193
+ });
2194
+ });
package/src/lookup.ts CHANGED
@@ -186,8 +186,8 @@ export interface LookupObjectOptions {
186
186
  signal?: AbortSignal;
187
187
 
188
188
  /**
189
- * The [FEP-ef61] proof policy to apply to portable objects, typically
190
- * `verifyPortableObjectProof()` from `@fedify/fedify`, which
189
+ * The [FEP-ef61] policy to apply to portable objects, typically
190
+ * `verifyPortableObject()` from `@fedify/fedify`, which
191
191
  * `Context.lookupObject()` uses by default.
192
192
  *
193
193
  * When it is given, portable `ap:`/`ap+ef61:` identifiers and compatible
@@ -206,6 +206,9 @@ export interface LookupObjectOptions {
206
206
  * a portable `@id` are refused as cross-origin objects, even with
207
207
  * `crossOrigin: "trust"`.
208
208
  *
209
+ * The returned object also uses it by default for dereferencing its
210
+ * properties, as if it were passed to its property accessors.
211
+ *
209
212
  * [FEP-ef61]: https://w3id.org/fep/ef61
210
213
  * @since 2.4.0
211
214
  */
@@ -456,6 +459,7 @@ async function lookupObjectInternal(
456
459
  documentLoader,
457
460
  contextLoader: snapshot?.loader ?? options.contextLoader,
458
461
  tracerProvider: options.tracerProvider,
462
+ verifyPortableObject: options.verifyPortableObject,
459
463
  baseUrl: documentUrl,
460
464
  });
461
465
  } catch (error) {
@@ -670,6 +674,7 @@ async function dereference(
670
674
  documentLoader,
671
675
  contextLoader,
672
676
  tracerProvider,
677
+ verifyPortableObject: options.verifyPortableObject,
673
678
  baseUrl,
674
679
  }),
675
680
  });
@@ -46,5 +46,5 @@ export const normalizeLinkToImage: PropertyPreprocessor<Image> = async (
46
46
  width: link.width,
47
47
  height: link.height,
48
48
  digestMultibase: link.digestMultibase,
49
- });
49
+ }, context);
50
50
  };