@fedify/vocab 2.4.0-pr.936.41 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +31328 -8723
  3. package/dist/mod.d.cts +4599 -6
  4. package/dist/mod.d.ts +4599 -6
  5. package/dist/mod.js +31221 -8623
  6. package/dist-tests/actor-D7_iX4I4.mjs +276 -0
  7. package/dist-tests/actor.test.mjs +55 -49
  8. package/dist-tests/{actor-MBpzF3-D.mjs → esm-DP2Spt0u.mjs} +15 -211
  9. package/dist-tests/gateway.test.mjs +1906 -0
  10. package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
  11. package/dist-tests/lookup.test.mjs +10 -268
  12. package/dist-tests/object.yaml +15 -0
  13. package/dist-tests/portable-lookup.test.mjs +653 -0
  14. package/dist-tests/signed-representation.test.mjs +189 -0
  15. package/dist-tests/translation.test.mjs +404 -0
  16. package/dist-tests/translation.yaml +91 -0
  17. package/dist-tests/type.test.mjs +2 -2
  18. package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-COZcxf00.mjs} +30831 -8515
  19. package/dist-tests/vocab.test.mjs +677 -10
  20. package/package.json +5 -5
  21. package/src/__snapshots__/vocab.test.ts.snap +291 -15
  22. package/src/actor.test.ts +41 -40
  23. package/src/actor.ts +120 -1
  24. package/src/gateway.test.ts +2637 -0
  25. package/src/lookup.test.ts +6 -5
  26. package/src/lookup.ts +460 -33
  27. package/src/mod.ts +9 -0
  28. package/src/object.yaml +15 -0
  29. package/src/portable-lookup.test.ts +889 -0
  30. package/src/preprocessors.ts +1 -1
  31. package/src/signed-representation.test.ts +277 -0
  32. package/src/translation.test.ts +518 -0
  33. package/src/translation.yaml +91 -0
  34. package/src/vocab.test.ts +914 -5
  35. /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
  36. /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
package/src/actor.test.ts CHANGED
@@ -99,45 +99,46 @@ test({
99
99
  async fn(t) {
100
100
  fetchMock.spyGlobal();
101
101
 
102
+ // Node validates addresses before fetch; public IP literals avoid DNS.
102
103
  fetchMock.get(
103
- "begin:https://foo.example.com/.well-known/webfinger?",
104
+ "begin:https://1.1.1.1/.well-known/webfinger?",
104
105
  {
105
- body: { subject: "acct:johndoe@foo.example.com" },
106
+ body: { subject: "acct:johndoe@1.1.1.1" },
106
107
  headers: { "Content-Type": "application/jrd+json" },
107
108
  },
108
109
  );
109
110
 
110
- const actorId = new URL("https://foo.example.com/@john");
111
+ const actorId = new URL("https://1.1.1.1/@john");
111
112
  const actor = new Person({
112
113
  id: actorId,
113
114
  preferredUsername: "john",
114
115
  });
115
116
 
116
117
  await t.step("WebFinger subject", async () => {
117
- deepStrictEqual(await getActorHandle(actor), "@johndoe@foo.example.com");
118
+ deepStrictEqual(await getActorHandle(actor), "@johndoe@1.1.1.1");
118
119
  deepStrictEqual(
119
120
  await getActorHandle(actor, { trimLeadingAt: true }),
120
- "johndoe@foo.example.com",
121
+ "johndoe@1.1.1.1",
121
122
  );
122
123
  deepStrictEqual(
123
124
  await getActorHandle(actorId),
124
- "@johndoe@foo.example.com",
125
+ "@johndoe@1.1.1.1",
125
126
  );
126
127
  deepStrictEqual(
127
128
  await getActorHandle(actorId, { trimLeadingAt: true }),
128
- "johndoe@foo.example.com",
129
+ "johndoe@1.1.1.1",
129
130
  );
130
131
  });
131
132
 
132
133
  fetchMock.removeRoutes();
133
134
  fetchMock.get(
134
- "begin:https://foo.example.com/.well-known/webfinger?",
135
+ "begin:https://1.1.1.1/.well-known/webfinger?",
135
136
  {
136
137
  body: {
137
- subject: "https://foo.example.com/@john",
138
+ subject: "https://1.1.1.1/@john",
138
139
  aliases: [
139
- "acct:john@bar.example.com",
140
- "acct:johndoe@foo.example.com",
140
+ "acct:john@8.8.8.8",
141
+ "acct:johndoe@1.1.1.1",
141
142
  ],
142
143
  },
143
144
  headers: { "Content-Type": "application/jrd+json" },
@@ -145,28 +146,28 @@ test({
145
146
  );
146
147
 
147
148
  await t.step("WebFinger aliases", async () => {
148
- deepStrictEqual(await getActorHandle(actor), "@johndoe@foo.example.com");
149
+ deepStrictEqual(await getActorHandle(actor), "@johndoe@1.1.1.1");
149
150
  deepStrictEqual(
150
151
  await getActorHandle(actor, { trimLeadingAt: true }),
151
- "johndoe@foo.example.com",
152
+ "johndoe@1.1.1.1",
152
153
  );
153
154
  deepStrictEqual(
154
155
  await getActorHandle(actorId),
155
- "@johndoe@foo.example.com",
156
+ "@johndoe@1.1.1.1",
156
157
  );
157
158
  deepStrictEqual(
158
159
  await getActorHandle(actorId, { trimLeadingAt: true }),
159
- "johndoe@foo.example.com",
160
+ "johndoe@1.1.1.1",
160
161
  );
161
162
  });
162
163
 
163
164
  fetchMock.get(
164
- "begin:https://bar.example.com/.well-known/webfinger?",
165
+ "begin:https://8.8.8.8/.well-known/webfinger?",
165
166
  {
166
167
  body: {
167
- subject: "acct:john@bar.example.com",
168
+ subject: "acct:john@8.8.8.8",
168
169
  aliases: [
169
- "https://foo.example.com/@john",
170
+ "https://1.1.1.1/@john",
170
171
  ],
171
172
  },
172
173
  headers: { "Content-Type": "application/jrd+json" },
@@ -174,18 +175,18 @@ test({
174
175
  );
175
176
 
176
177
  await t.step("cross-origin WebFinger resources", async () => {
177
- deepStrictEqual(await getActorHandle(actor), "@john@bar.example.com");
178
+ deepStrictEqual(await getActorHandle(actor), "@john@8.8.8.8");
178
179
  });
179
180
 
180
181
  fetchMock.removeRoutes();
181
182
  fetchMock.get(
182
- "begin:https://foo.example.com/.well-known/webfinger?",
183
+ "begin:https://1.1.1.1/.well-known/webfinger?",
183
184
  { status: 404 },
184
185
  );
185
186
 
186
187
  await t.step("no WebFinger", async () => {
187
- deepStrictEqual(await getActorHandle(actor), "@john@foo.example.com");
188
- rejects(() => getActorHandle(actorId), TypeError);
188
+ deepStrictEqual(await getActorHandle(actor), "@john@1.1.1.1");
189
+ await rejects(() => getActorHandle(actorId), TypeError);
189
190
  });
190
191
 
191
192
  fetchMock.hardReset();
@@ -197,7 +198,7 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
197
198
  }, async (t) => {
198
199
  fetchMock.spyGlobal();
199
200
  try {
200
- const actorId = new URL("https://foo.example.com/@john");
201
+ const actorId = new URL("https://1.1.1.1/@john");
201
202
  const actor = new Person({
202
203
  id: actorId,
203
204
  preferredUsername: "john",
@@ -206,15 +207,15 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
206
207
  await t.step("records result=resolved on a successful lookup", async () => {
207
208
  fetchMock.removeRoutes();
208
209
  fetchMock.get(
209
- "begin:https://foo.example.com/.well-known/webfinger?",
210
+ "begin:https://1.1.1.1/.well-known/webfinger?",
210
211
  {
211
- body: { subject: "acct:johndoe@foo.example.com" },
212
+ body: { subject: "acct:johndoe@1.1.1.1" },
212
213
  headers: { "Content-Type": "application/jrd+json" },
213
214
  },
214
215
  );
215
216
  const [meterProvider, recorder] = createTestMeterProvider();
216
217
  const handle = await getActorHandle(actor, { meterProvider });
217
- deepStrictEqual(handle, "@johndoe@foo.example.com");
218
+ deepStrictEqual(handle, "@johndoe@1.1.1.1");
218
219
 
219
220
  const counters = recorder.getMeasurements(
220
221
  "activitypub.actor.discovery",
@@ -228,7 +229,7 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
228
229
  );
229
230
  deepStrictEqual(
230
231
  counters[0].attributes["activitypub.remote.host"],
231
- "foo.example.com",
232
+ "1.1.1.1",
232
233
  );
233
234
 
234
235
  const durations = recorder.getMeasurements(
@@ -248,12 +249,12 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
248
249
  async () => {
249
250
  fetchMock.removeRoutes();
250
251
  fetchMock.get(
251
- "begin:https://foo.example.com/.well-known/webfinger?",
252
+ "begin:https://1.1.1.1/.well-known/webfinger?",
252
253
  { status: 404 },
253
254
  );
254
255
  const [meterProvider, recorder] = createTestMeterProvider();
255
256
  const handle = await getActorHandle(actor, { meterProvider });
256
- deepStrictEqual(handle, "@john@foo.example.com");
257
+ deepStrictEqual(handle, "@john@1.1.1.1");
257
258
  const counter = recorder.getMeasurement("activitypub.actor.discovery");
258
259
  ok(counter != null);
259
260
  deepStrictEqual(
@@ -268,7 +269,7 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
268
269
  async () => {
269
270
  fetchMock.removeRoutes();
270
271
  fetchMock.get(
271
- "begin:https://foo.example.com/.well-known/webfinger?",
272
+ "begin:https://1.1.1.1/.well-known/webfinger?",
272
273
  { status: 404 },
273
274
  );
274
275
  const [meterProvider, recorder] = createTestMeterProvider();
@@ -284,7 +285,7 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
284
285
  );
285
286
  deepStrictEqual(
286
287
  counter.attributes["activitypub.remote.host"],
287
- "foo.example.com",
288
+ "1.1.1.1",
288
289
  );
289
290
  const duration = recorder.getMeasurement(
290
291
  "activitypub.actor.discovery.duration",
@@ -302,13 +303,13 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
302
303
  async () => {
303
304
  fetchMock.removeRoutes();
304
305
  fetchMock.get(
305
- "begin:https://foo.example.com:8443/.well-known/webfinger?",
306
+ "begin:https://1.1.1.1:8443/.well-known/webfinger?",
306
307
  { status: 404 },
307
308
  );
308
309
  const [meterProvider, recorder] = createTestMeterProvider();
309
310
  await rejects(
310
311
  () =>
311
- getActorHandle(new URL("https://foo.example.com:8443/@john"), {
312
+ getActorHandle(new URL("https://1.1.1.1:8443/@john"), {
312
313
  meterProvider,
313
314
  }),
314
315
  TypeError,
@@ -317,7 +318,7 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
317
318
  ok(counter != null);
318
319
  deepStrictEqual(
319
320
  counter.attributes["activitypub.remote.host"],
320
- "foo.example.com:8443",
321
+ "1.1.1.1:8443",
321
322
  );
322
323
  },
323
324
  );
@@ -332,10 +333,10 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
332
333
  // `error` rather than `not_found`.
333
334
  fetchMock.removeRoutes();
334
335
  fetchMock.get(
335
- "begin:https://foo.example.com/.well-known/webfinger?",
336
+ "begin:https://1.1.1.1/.well-known/webfinger?",
336
337
  {
337
338
  body: {
338
- subject: "https://foo.example.com/@john",
339
+ subject: "https://1.1.1.1/@john",
339
340
  aliases: ["acct:john@["],
340
341
  },
341
342
  headers: { "Content-Type": "application/jrd+json" },
@@ -362,9 +363,9 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
362
363
  async () => {
363
364
  fetchMock.removeRoutes();
364
365
  fetchMock.get(
365
- "begin:https://foo.example.com/.well-known/webfinger?",
366
+ "begin:https://1.1.1.1/.well-known/webfinger?",
366
367
  {
367
- body: { subject: "acct:johndoe@foo.example.com" },
368
+ body: { subject: "acct:johndoe@1.1.1.1" },
368
369
  headers: { "Content-Type": "application/jrd+json" },
369
370
  },
370
371
  );
@@ -384,9 +385,9 @@ test("getActorHandle() records activitypub.actor.discovery counter", {
384
385
  async () => {
385
386
  fetchMock.removeRoutes();
386
387
  fetchMock.get(
387
- "begin:https://foo.example.com/.well-known/webfinger?",
388
+ "begin:https://1.1.1.1/.well-known/webfinger?",
388
389
  {
389
- body: { subject: "acct:johndoe@foo.example.com" },
390
+ body: { subject: "acct:johndoe@1.1.1.1" },
390
391
  headers: { "Content-Type": "application/jrd+json" },
391
392
  },
392
393
  );
package/src/actor.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { GetUserAgentOptions } from "@fedify/vocab-runtime";
2
- import { lookupWebFinger } from "@fedify/webfinger";
2
+ import {
3
+ canonicalizePortableUri,
4
+ formatIri,
5
+ fromCompatibleEf61Id,
6
+ isGatewayUrl,
7
+ } from "@fedify/vocab-runtime";
8
+ import { lookupWebFinger, type ResourceDescriptor } from "@fedify/webfinger";
3
9
  import {
4
10
  type Attributes,
5
11
  type Counter,
@@ -93,6 +99,11 @@ function getActorDiscoveryRemoteHost(
93
99
  ): string | undefined {
94
100
  const id = actor instanceof URL ? actor : actor.id;
95
101
  if (id == null) return undefined;
102
+ // The authority of a portable ID is a DID, not a host; the handle of
103
+ // a portable actor comes from its first gateway:
104
+ if (isPortableActorId(id)) {
105
+ return actor instanceof URL ? undefined : actor.gateway?.host;
106
+ }
96
107
  return id.host === "" ? undefined : id.host;
97
108
  }
98
109
 
@@ -234,6 +245,17 @@ export interface GetActorHandleOptions extends NormalizeActorHandleOptions {
234
245
  * await getActorHandle(new URL("https://fosstodon.org/users/hongminhee"));
235
246
  * ```
236
247
  *
248
+ * For an [FEP-ef61] portable actor, whose ID is an `ap:` or `ap+ef61:` URI,
249
+ * the domain is taken from the first gateway in the actor's `gateways` rather
250
+ * than from its ID. The handle is returned only if its WebFinger response
251
+ * links back to the actor, since the actor's `gateways` are claimed by the
252
+ * actor itself; a failed WebFinger lookup is not enough information to get
253
+ * the handle. A portable actor without `gateways` or
254
+ * `preferredUsername`, or a portable actor URI (which does not tell its
255
+ * gateways), is not supported.
256
+ *
257
+ * [FEP-ef61]: https://w3id.org/fep/ef61
258
+ *
237
259
  * @param actor The actor or actor URI to get the handle from.
238
260
  * @param options The extra options for getting the actor handle.
239
261
  * @returns The actor handle. It starts with `@` and is followed by the
@@ -302,6 +324,10 @@ async function getActorHandleInternal(
302
324
  options: GetActorHandleOptions = {},
303
325
  ): Promise<`@${string}@${string}` | `${string}@${string}`> {
304
326
  const actorId = actor instanceof URL ? actor : actor.id;
327
+ if (actorId != null && isPortableActorId(actorId)) {
328
+ if (actor instanceof URL) throw new ActorHandleNotFoundError();
329
+ return await getPortableActorHandle(actor, actorId, options);
330
+ }
305
331
  if (actorId != null) {
306
332
  const result = await lookupWebFinger(actorId, {
307
333
  userAgent: options.userAgent,
@@ -344,6 +370,89 @@ async function getActorHandleInternal(
344
370
  throw new ActorHandleNotFoundError();
345
371
  }
346
372
 
373
+ function isPortableActorId(id: URL): boolean {
374
+ return id.protocol === "ap:" || id.protocol === "ap+ef61:";
375
+ }
376
+
377
+ /**
378
+ * The reverse discovery of an FEP-ef61 portable actor's WebFinger address,
379
+ * which takes the domain from the first gateway in its `gateways`.
380
+ */
381
+ async function getPortableActorHandle(
382
+ actor: Actor,
383
+ actorId: URL,
384
+ options: GetActorHandleOptions,
385
+ ): Promise<`@${string}@${string}` | `${string}@${string}`> {
386
+ const gateway = actor.gateway;
387
+ const username = actor.preferredUsername?.toString();
388
+ if (gateway == null || !isGatewayUrl(gateway) || username == null) {
389
+ throw new ActorHandleNotFoundError();
390
+ }
391
+ const canonicalId = canonicalizePortableUri(formatIri(actorId));
392
+ const handle = `acct:${username}@${gateway.host}`;
393
+ const result = await lookupWebFinger(handle, {
394
+ userAgent: options.userAgent,
395
+ tracerProvider: options.tracerProvider,
396
+ meterProvider: options.meterProvider,
397
+ });
398
+ // Unlike an ordinary actor, whose ID vouches for its host, a portable
399
+ // actor's gateways are claimed by the actor itself, so there is no
400
+ // fallback when the gateway does not link back to the actor:
401
+ if (result == null || !linksToPortableActor(result, canonicalId)) {
402
+ throw new ActorHandleNotFoundError();
403
+ }
404
+ // The subject is the canonical address when it differs from the queried
405
+ // one; follow it once if it also links back to the actor:
406
+ const subject = result.subject?.match(/^acct:([^@]+)@([^@]+)$/);
407
+ if (subject != null && result.subject !== handle) {
408
+ const subjectResult = await lookupWebFinger(result.subject!, {
409
+ userAgent: options.userAgent,
410
+ tracerProvider: options.tracerProvider,
411
+ meterProvider: options.meterProvider,
412
+ });
413
+ if (
414
+ subjectResult != null && linksToPortableActor(subjectResult, canonicalId)
415
+ ) {
416
+ return normalizeActorHandle(`@${subject[1]}@${subject[2]}`, options);
417
+ }
418
+ }
419
+ return normalizeActorHandle(`@${username}@${gateway.host}`, options);
420
+ }
421
+
422
+ /**
423
+ * Checks if the first ActivityStreams `self` link of a WebFinger response
424
+ * identifies the portable actor, either by its portable ID or by
425
+ * a compatible identifier.
426
+ */
427
+ function linksToPortableActor(
428
+ jrd: ResourceDescriptor,
429
+ canonicalId: string,
430
+ ): boolean {
431
+ const link = jrd.links?.find((l) =>
432
+ l.rel === "self" && l.href != null &&
433
+ (l.type === "application/activity+json" ||
434
+ l.type?.match(
435
+ /application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/,
436
+ ) != null)
437
+ );
438
+ if (link?.href == null) return false;
439
+ try {
440
+ let id: string;
441
+ if (/^ap(?:\+ef61)?:/i.test(link.href)) id = link.href;
442
+ else {
443
+ const portable = URL.canParse(link.href)
444
+ ? fromCompatibleEf61Id(link.href)
445
+ : null;
446
+ if (portable == null) return false;
447
+ id = formatIri(portable);
448
+ }
449
+ return canonicalizePortableUri(id) === canonicalId;
450
+ } catch (error) {
451
+ if (error instanceof TypeError) return false;
452
+ throw error;
453
+ }
454
+ }
455
+
347
456
  async function verifyCrossOriginActorHandle(
348
457
  actorId: string,
349
458
  alias: string,
@@ -432,4 +541,14 @@ export interface Recipient {
432
541
  */
433
542
  readonly sharedInbox: URL | null;
434
543
  } | null;
544
+
545
+ /**
546
+ * The [FEP-ef61] gateways of the actor, in order, if it is a portable actor.
547
+ * Activities to a portable inbox, i.e., an `ap:` or `ap+ef61:` URI, are
548
+ * delivered to the inbox's compatible identifier on one of these gateways.
549
+ *
550
+ * [FEP-ef61]: https://w3id.org/fep/ef61
551
+ * @since 2.4.0
552
+ */
553
+ readonly gateways?: readonly URL[];
435
554
  }