@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.36

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 (138) hide show
  1. package/README.md +16 -7
  2. package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
  3. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
  4. package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
  5. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
  6. package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
  7. package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
  8. package/dist/{builder-yAlpiIqP.mjs → builder-DSZ7FZmg.mjs} +96 -42
  9. package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
  10. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  11. package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
  12. package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
  13. package/dist/compat/mod.d.cts +2 -1
  14. package/dist/compat/mod.d.ts +2 -3
  15. package/dist/compat/mod.js +3 -3
  16. package/dist/compat/outgoing-jsonld.test.mjs +4 -4
  17. package/dist/compat/public-audience.test.mjs +4 -4
  18. package/dist/compat/transformers.test.mjs +5 -5
  19. package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
  20. package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
  21. package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
  22. package/dist/{deno-XBVlKnOX.mjs → deno-DCdM7d93.mjs} +1 -1
  23. package/dist/{docloader-DPp-X6gk.mjs → docloader-B7mVwA5_.mjs} +4 -4
  24. package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
  25. package/dist/federation/builder.test.mjs +163 -11
  26. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  27. package/dist/federation/circuit-breaker.test.mjs +446 -0
  28. package/dist/federation/collection.test.mjs +3 -3
  29. package/dist/federation/handler.test.mjs +1770 -29
  30. package/dist/federation/idempotency.test.mjs +5 -5
  31. package/dist/federation/inbox.test.mjs +4 -4
  32. package/dist/federation/keycache.test.mjs +5 -5
  33. package/dist/federation/kv.test.mjs +2 -2
  34. package/dist/federation/metrics.test.d.mts +2 -0
  35. package/dist/federation/metrics.test.mjs +634 -0
  36. package/dist/federation/middleware.test.mjs +4036 -146
  37. package/dist/federation/mod.cjs +195 -14
  38. package/dist/federation/mod.d.cts +6 -4
  39. package/dist/federation/mod.d.ts +6 -6
  40. package/dist/federation/mod.js +192 -14
  41. package/dist/federation/mq.test.mjs +176 -15
  42. package/dist/federation/negotiation.test.mjs +4 -4
  43. package/dist/federation/retry.test.mjs +3 -3
  44. package/dist/federation/router.test.mjs +190 -9
  45. package/dist/federation/send.test.mjs +153 -15
  46. package/dist/federation/temporal.test.d.mts +2 -0
  47. package/dist/federation/temporal.test.mjs +71 -0
  48. package/dist/federation/webfinger.test.mjs +150 -5
  49. package/dist/{http-CSlLA54v.mjs → http-BufbkLuW.mjs} +146 -47
  50. package/dist/{http-B4rrzxtg.js → http-Dh18nwJg.js} +994 -64
  51. package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
  52. package/dist/{http-BrGqJDXL.cjs → http-gOiudB3g.cjs} +1099 -61
  53. package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
  54. package/dist/{key-Bzx--sHD.mjs → key-DZdKLJXT.mjs} +43 -18
  55. package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
  56. package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BuK_nlpY.cjs} +20 -3
  57. package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-C7SQnkGI.mjs} +21 -3
  58. package/dist/{kv-cache-EMIqoIuB.js → kv-cache-DNDSb6Po.js} +20 -3
  59. package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
  60. package/dist/ld-0Lsznacf.mjs +626 -0
  61. package/dist/metrics-BHeWd24f.mjs +815 -0
  62. package/dist/{middleware-zjZ6AFCW.mjs → middleware-BNANvDh2.mjs} +1 -1
  63. package/dist/{middleware-nidH7VZH.js → middleware-BgzRkDcm.js} +2260 -556
  64. package/dist/{middleware-An4DjES4.cjs → middleware-D-iES1nQ.cjs} +2318 -623
  65. package/dist/{middleware-K1g6bEkV.mjs → middleware-EV6J_eAc.mjs} +1639 -373
  66. package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
  67. package/dist/mod-C0F6kvgS.d.cts +182 -0
  68. package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
  69. package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
  70. package/dist/mod-vPYVoa5n.d.ts +182 -0
  71. package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
  72. package/dist/mod.cjs +9 -6
  73. package/dist/mod.d.cts +12 -10
  74. package/dist/mod.d.ts +12 -12
  75. package/dist/mod.js +11 -11
  76. package/dist/mq-D-nlpY04.d.ts +208 -0
  77. package/dist/mq-D8uSFzxe.d.cts +208 -0
  78. package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
  79. package/dist/nodeinfo/client.test.mjs +4 -4
  80. package/dist/nodeinfo/handler.test.mjs +4 -4
  81. package/dist/nodeinfo/mod.d.cts +2 -1
  82. package/dist/nodeinfo/mod.d.ts +2 -3
  83. package/dist/nodeinfo/mod.js +3 -3
  84. package/dist/nodeinfo/types.test.mjs +3 -3
  85. package/dist/otel/exporter.test.mjs +28 -25
  86. package/dist/otel/mod.cjs +6 -5
  87. package/dist/otel/mod.d.cts +5 -3
  88. package/dist/otel/mod.d.ts +5 -5
  89. package/dist/otel/mod.js +8 -7
  90. package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
  91. package/dist/{owner-C-tqfvxn.mjs → owner-Cl-1cAR8.mjs} +2 -2
  92. package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
  93. package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
  94. package/dist/{proof-Bbbwf8_x.js → proof-CyXndf6-.js} +378 -15
  95. package/dist/{proof-BlmRPzrK.mjs → proof-Dnz8jtiQ.mjs} +26 -8
  96. package/dist/{proof-BW89QMVB.cjs → proof-tz35unOj.cjs} +432 -15
  97. package/dist/runtime/mod.d.cts +1 -0
  98. package/dist/runtime/mod.d.ts +1 -2
  99. package/dist/runtime/mod.js +3 -3
  100. package/dist/{send-05pJLcb6.mjs → send-DIbBYN5P.mjs} +72 -26
  101. package/dist/sig/http.test.mjs +356 -33
  102. package/dist/sig/key.test.mjs +103 -6
  103. package/dist/sig/ld.test.mjs +696 -7
  104. package/dist/sig/mod.cjs +2 -2
  105. package/dist/sig/mod.d.cts +4 -3
  106. package/dist/sig/mod.d.ts +4 -5
  107. package/dist/sig/mod.js +4 -4
  108. package/dist/sig/owner.test.mjs +6 -6
  109. package/dist/sig/proof.test.mjs +169 -8
  110. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
  111. package/dist/temporal-BEwob1Vg.mjs +95 -0
  112. package/dist/testing/mod.d.mts +110 -10
  113. package/dist/testing/mod.mjs +1 -2
  114. package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
  115. package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
  116. package/dist/utils/docloader.test.mjs +6 -6
  117. package/dist/utils/kv-cache.test.mjs +67 -2
  118. package/dist/utils/mod.cjs +1 -1
  119. package/dist/utils/mod.d.cts +2 -1
  120. package/dist/utils/mod.d.ts +2 -3
  121. package/dist/utils/mod.js +3 -3
  122. package/dist/vocab/cjs.test.mjs +1 -1
  123. package/dist/vocab/mod.d.cts +1 -0
  124. package/dist/vocab/mod.d.ts +1 -2
  125. package/dist/vocab/mod.js +2 -2
  126. package/package.json +16 -16
  127. package/dist/ld-DR78beiv.mjs +0 -279
  128. package/dist/middleware-BFBaR0mF.cjs +0 -4
  129. package/dist/mod-2d12ffz3.d.ts +0 -64
  130. package/dist/mod-D35TRn09.d.cts +0 -62
  131. package/dist/router-CrMLXoOr.mjs +0 -114
  132. package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
  133. package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
  134. package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
  135. package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
  136. package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
  137. package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
  138. /package/dist/{retry-bMXBL97A.mjs → retry-CXg_MBI-.mjs} +0 -0
@@ -1,17 +1,25 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
6
- import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
7
- import { t as assert } from "../assert-DikXweDx.mjs";
8
- import { i as generateCryptoKeyPair } from "../key-Bzx--sHD.mjs";
4
+ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
+ import { n as assertGreaterOrEqual, r as assertFalse, t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
6
+ import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
7
+ import { t as assert } from "../assert-OguE97r2.mjs";
8
+ import { i as generateCryptoKeyPair } from "../key-DZdKLJXT.mjs";
9
9
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
10
- import { a as signJsonLd, i as hasSignatureLike, n as createSignature, o as verifyJsonLd, r as detachSignature, s as verifySignature, t as attachSignature } from "../ld-DR78beiv.mjs";
11
- import { mockDocumentLoader, test } from "@fedify/fixture";
10
+ import { a as compactJsonLd, f as isInvalidUrlTypeError, g as verifySignature, h as verifyJsonLd, i as attachSignature, n as UnsafeJsonLdError, o as createSignature, p as signJsonLd, s as detachSignature, u as hasSignatureLike } from "../ld-0Lsznacf.mjs";
12
11
  import { CryptographicKey } from "@fedify/vocab";
12
+ import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
13
13
  import { encodeBase64 } from "byte-encodings/base64";
14
14
  //#region src/sig/ld.test.ts
15
+ test("isInvalidUrlTypeError()", () => {
16
+ assert(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("Invalid URL: http://[")));
17
+ assert(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("\"http://[\" cannot be parsed as a URL.")));
18
+ const error = /* @__PURE__ */ new TypeError("Failed to parse URL");
19
+ error.code = "ERR_INVALID_URL";
20
+ assert(isInvalidUrlTypeError(error));
21
+ assertFalse(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("Failed to fetch")));
22
+ });
15
23
  test("attachSignature()", () => {
16
24
  const sig = {
17
25
  "@context": "https://w3id.org/identity/v1",
@@ -249,5 +257,686 @@ test("verifyJsonLd()", async () => {
249
257
  contextLoader: mockDocumentLoader
250
258
  }));
251
259
  });
260
+ test("verifyJsonLd() records verification duration metric", async (t) => {
261
+ await t.step("verified path records result=verified with bounded type", async () => {
262
+ const [meterProvider, recorder] = createTestMeterProvider();
263
+ assert(await verifyJsonLd(testVector, {
264
+ documentLoader: mockDocumentLoader,
265
+ contextLoader: mockDocumentLoader,
266
+ meterProvider
267
+ }));
268
+ const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
269
+ assertEquals(measurements.length, 1);
270
+ const m = measurements[0];
271
+ assertEquals(m.type, "histogram");
272
+ assertGreaterOrEqual(m.value, 0);
273
+ assertEquals(m.attributes["activitypub.signature.kind"], "linked_data");
274
+ assertEquals(m.attributes["activitypub.signature.result"], "verified");
275
+ assertEquals(m.attributes["ld_signatures.type"], "RsaSignature2017");
276
+ });
277
+ await t.step("missing signature records result=missing without type", async () => {
278
+ const [meterProvider, recorder] = createTestMeterProvider();
279
+ assertFalse(await verifyJsonLd(document, {
280
+ documentLoader: mockDocumentLoader,
281
+ contextLoader: mockDocumentLoader,
282
+ meterProvider
283
+ }));
284
+ const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
285
+ assertEquals(measurements.length, 1);
286
+ assertEquals(measurements[0].attributes["activitypub.signature.kind"], "linked_data");
287
+ assertEquals(measurements[0].attributes["activitypub.signature.result"], "missing");
288
+ assertFalse("ld_signatures.type" in measurements[0].attributes);
289
+ });
290
+ await t.step("invalid signature records result=rejected", async () => {
291
+ const [meterProvider, recorder] = createTestMeterProvider();
292
+ assertFalse(await verifyJsonLd({
293
+ ...testVector,
294
+ signature: {
295
+ ...testVector.signature,
296
+ signatureValue: encodeBase64(new Uint8Array([
297
+ 1,
298
+ 2,
299
+ 3,
300
+ 4
301
+ ]))
302
+ }
303
+ }, {
304
+ documentLoader: mockDocumentLoader,
305
+ contextLoader: mockDocumentLoader,
306
+ meterProvider
307
+ }));
308
+ const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
309
+ assertEquals(measurements.length, 1);
310
+ assertEquals(measurements[0].attributes["activitypub.signature.result"], "rejected");
311
+ assertEquals(measurements[0].attributes["ld_signatures.type"], "RsaSignature2017");
312
+ });
313
+ await t.step("malformed (null) signature property records result=rejected, not missing", async () => {
314
+ const [meterProvider, recorder] = createTestMeterProvider();
315
+ assertFalse(await verifyJsonLd({
316
+ ...document,
317
+ signature: null
318
+ }, {
319
+ documentLoader: mockDocumentLoader,
320
+ contextLoader: mockDocumentLoader,
321
+ meterProvider
322
+ }));
323
+ const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
324
+ assertEquals(measurements.length, 1);
325
+ assertEquals(measurements[0].attributes["activitypub.signature.result"], "rejected");
326
+ assertFalse("ld_signatures.type" in measurements[0].attributes);
327
+ });
328
+ await t.step("key fetch records result=fetched on a cold cache", async () => {
329
+ const [meterProvider, recorder] = createTestMeterProvider();
330
+ assert(await verifyJsonLd(testVector, {
331
+ documentLoader: mockDocumentLoader,
332
+ contextLoader: mockDocumentLoader,
333
+ meterProvider
334
+ }));
335
+ const measurements = recorder.getMeasurements("activitypub.signature.key_fetch.duration");
336
+ assertEquals(measurements.length, 1);
337
+ assertGreaterOrEqual(measurements[0].value, 0);
338
+ assertEquals(measurements[0].attributes["activitypub.signature.kind"], "linked_data");
339
+ assertEquals(measurements[0].attributes["activitypub.signature.key_fetch.result"], "fetched");
340
+ });
341
+ await t.step("missing signature emits no key_fetch measurement", async () => {
342
+ const [meterProvider, recorder] = createTestMeterProvider();
343
+ assertFalse(await verifyJsonLd(document, {
344
+ documentLoader: mockDocumentLoader,
345
+ contextLoader: mockDocumentLoader,
346
+ meterProvider
347
+ }));
348
+ assertEquals(recorder.getMeasurements("activitypub.signature.key_fetch.duration").length, 0);
349
+ });
350
+ await t.step("cached-key retry emits two key_fetch measurements: hit then fetched", async () => {
351
+ const [meterProvider, recorder] = createTestMeterProvider();
352
+ assert(await verifyJsonLd(testVector, {
353
+ documentLoader: mockDocumentLoader,
354
+ contextLoader: mockDocumentLoader,
355
+ meterProvider,
356
+ keyCache: {
357
+ async get(keyId) {
358
+ return new CryptographicKey({
359
+ id: keyId,
360
+ owner: new URL("https://activitypub.academy/users/brauca_darradiul"),
361
+ publicKey: (await generateCryptoKeyPair("RSASSA-PKCS1-v1_5")).publicKey
362
+ });
363
+ },
364
+ set(_keyId, _key) {
365
+ return Promise.resolve();
366
+ }
367
+ }
368
+ }));
369
+ const measurements = recorder.getMeasurements("activitypub.signature.key_fetch.duration");
370
+ assertEquals(measurements.length, 2);
371
+ assertEquals(measurements[0].attributes["activitypub.signature.key_fetch.result"], "hit");
372
+ assertEquals(measurements[1].attributes["activitypub.signature.key_fetch.result"], "fetched");
373
+ });
374
+ await t.step("unknown signature type omits the ld_signatures.type metric attribute", async () => {
375
+ const [meterProvider, recorder] = createTestMeterProvider();
376
+ assertFalse(await verifyJsonLd({
377
+ ...testVector,
378
+ signature: {
379
+ ...testVector.signature,
380
+ type: "MadeUpSignature9999"
381
+ }
382
+ }, {
383
+ documentLoader: mockDocumentLoader,
384
+ contextLoader: mockDocumentLoader,
385
+ meterProvider
386
+ }));
387
+ const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
388
+ assertEquals(measurements.length, 1);
389
+ assertEquals(measurements[0].attributes["activitypub.signature.result"], "rejected");
390
+ assertFalse("ld_signatures.type" in measurements[0].attributes);
391
+ });
392
+ });
393
+ test("compactJsonLd() with restrictive context loader", async () => {
394
+ const restrictiveContextLoader = async (resource) => {
395
+ const url = new URL(resource).href;
396
+ if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
397
+ throw new Error(`Unexpected context: ${url}`);
398
+ };
399
+ const signed = await signJsonLd({
400
+ "@context": [
401
+ "https://www.w3.org/ns/activitystreams",
402
+ "https://w3id.org/identity/v1",
403
+ "https://w3id.org/security/v1",
404
+ "https://w3id.org/security/data-integrity/v1"
405
+ ],
406
+ id: "https://example.com/1",
407
+ type: "Create",
408
+ actor: "https://example.com/person2"
409
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
410
+ assertEquals(await compactJsonLd(signed, restrictiveContextLoader), {
411
+ "@context": [
412
+ "https://w3id.org/identity/v1",
413
+ "https://www.w3.org/ns/activitystreams",
414
+ "https://w3id.org/security/v1",
415
+ "https://w3id.org/security/data-integrity/v1"
416
+ ],
417
+ id: "https://example.com/1",
418
+ type: "Create",
419
+ actor: "https://example.com/person2",
420
+ signature: signed.signature
421
+ });
422
+ });
423
+ test("compactJsonLd() caches repeated remote contexts across graph scan and compaction", async () => {
424
+ const remoteUrl = "https://example.com/context";
425
+ let calls = 0;
426
+ const countingLoader = async (resource) => {
427
+ const url = new URL(resource).href;
428
+ if (url === remoteUrl) {
429
+ calls++;
430
+ return {
431
+ contextUrl: null,
432
+ documentUrl: url,
433
+ document: { "@context": { extra: "https://example.com/extra" } }
434
+ };
435
+ }
436
+ return await mockDocumentLoader(url);
437
+ };
438
+ await compactJsonLd({
439
+ "@context": "https://www.w3.org/ns/activitystreams",
440
+ id: "https://example.com/activities/remote-contexts",
441
+ type: "Create",
442
+ actor: "https://example.com/person2",
443
+ object: [
444
+ {
445
+ "@context": ["https://www.w3.org/ns/activitystreams", remoteUrl],
446
+ type: "Note",
447
+ content: "one"
448
+ },
449
+ {
450
+ "@context": ["https://www.w3.org/ns/activitystreams", remoteUrl],
451
+ type: "Note",
452
+ content: "two"
453
+ },
454
+ {
455
+ "@context": ["https://www.w3.org/ns/activitystreams", remoteUrl],
456
+ type: "Note",
457
+ content: "three"
458
+ }
459
+ ]
460
+ }, countingLoader);
461
+ assertEquals(calls, 1);
462
+ });
463
+ test("compactJsonLd() reuses the same remote context response for graph scan and compaction", async () => {
464
+ const remoteUrl = "https://example.com/context";
465
+ let calls = 0;
466
+ const compacted = await compactJsonLd({
467
+ "@context": [remoteUrl, "https://www.w3.org/ns/activitystreams"],
468
+ id: "https://example.com/activities/memoized-remote-context",
469
+ type: "Create",
470
+ actor: "https://example.com/person2",
471
+ graph: "https://example.com/custom-graph",
472
+ object: "https://example.com/notes/1"
473
+ }, async (resource) => {
474
+ const url = new URL(resource).href;
475
+ if (url === remoteUrl) {
476
+ calls++;
477
+ if (calls > 1) throw new Error(`Remote context should not be fetched twice: ${url}`);
478
+ return {
479
+ contextUrl: null,
480
+ documentUrl: url,
481
+ document: { "@context": { graph: "https://example.com/graph" } }
482
+ };
483
+ }
484
+ return await mockDocumentLoader(url);
485
+ });
486
+ assertEquals(calls, 1);
487
+ assertEquals(compacted, {
488
+ "@context": [
489
+ "https://w3id.org/identity/v1",
490
+ "https://www.w3.org/ns/activitystreams",
491
+ "https://w3id.org/security/v1",
492
+ "https://w3id.org/security/data-integrity/v1"
493
+ ],
494
+ id: "https://example.com/activities/memoized-remote-context",
495
+ type: "Create",
496
+ actor: "https://example.com/person2",
497
+ "https://example.com/graph": "https://example.com/custom-graph",
498
+ object: "https://example.com/notes/1"
499
+ });
500
+ });
501
+ test("compactJsonLd() preserves opaque top-level ids and resolves relative remote contexts against documentUrl during graph scan", async () => {
502
+ const rootContextId = "opaque-root";
503
+ const rootContextUrl = "https://example.com/contexts/root";
504
+ const childContextUrl = "https://example.com/contexts/child";
505
+ const calls = [];
506
+ const customLoader = async (resource) => {
507
+ calls.push(resource);
508
+ if (resource === rootContextId) return {
509
+ contextUrl: null,
510
+ documentUrl: rootContextUrl,
511
+ document: { "@context": {
512
+ "@import": "./child",
513
+ ext: "https://example.com/ext"
514
+ } }
515
+ };
516
+ if (resource === childContextUrl || resource === "child") return {
517
+ contextUrl: null,
518
+ documentUrl: childContextUrl,
519
+ document: { "@context": { child: "https://example.com/child" } }
520
+ };
521
+ return await mockDocumentLoader(resource);
522
+ };
523
+ assertEquals(await compactJsonLd({
524
+ "@context": [rootContextId, "https://www.w3.org/ns/activitystreams"],
525
+ id: "https://example.com/activities/custom-loader-contexts",
526
+ type: "Create",
527
+ actor: "https://example.com/person2",
528
+ ext: "preserve-me",
529
+ object: {
530
+ type: "Note",
531
+ content: "Hello"
532
+ }
533
+ }, customLoader), {
534
+ "@context": [
535
+ "https://w3id.org/identity/v1",
536
+ "https://www.w3.org/ns/activitystreams",
537
+ "https://w3id.org/security/v1",
538
+ "https://w3id.org/security/data-integrity/v1"
539
+ ],
540
+ id: "https://example.com/activities/custom-loader-contexts",
541
+ type: "Create",
542
+ actor: "https://example.com/person2",
543
+ "https://example.com/ext": "preserve-me",
544
+ object: {
545
+ type: "Note",
546
+ content: "Hello"
547
+ }
548
+ });
549
+ assert(calls.includes(rootContextId));
550
+ assert(calls.includes(childContextUrl));
551
+ assertFalse(calls.includes("./child"));
552
+ });
553
+ test("compactJsonLd() preserves base URLs for property-scoped remote contexts", async () => {
554
+ const rootContextId = "opaque-root";
555
+ const rootContextUrl = "https://example.com/contexts/root";
556
+ const childContextUrl = "https://example.com/contexts/child";
557
+ const calls = [];
558
+ const customLoader = async (resource) => {
559
+ calls.push(resource);
560
+ if (resource === rootContextId) return {
561
+ contextUrl: null,
562
+ documentUrl: rootContextUrl,
563
+ document: { "@context": { p: {
564
+ "@id": "https://example.com/p",
565
+ "@context": "./child"
566
+ } } }
567
+ };
568
+ if (resource === childContextUrl) return {
569
+ contextUrl: null,
570
+ documentUrl: childContextUrl,
571
+ document: { "@context": { nested: "https://example.com/nested" } }
572
+ };
573
+ return await mockDocumentLoader(resource);
574
+ };
575
+ assertEquals(await compactJsonLd({
576
+ "@context": [rootContextId, "https://www.w3.org/ns/activitystreams"],
577
+ id: "https://example.com/activities/property-scoped-contexts",
578
+ type: "Create",
579
+ actor: "https://example.com/person2",
580
+ p: { nested: "value" },
581
+ object: "https://example.com/notes/1"
582
+ }, customLoader), {
583
+ "@context": [
584
+ "https://w3id.org/identity/v1",
585
+ "https://www.w3.org/ns/activitystreams",
586
+ "https://w3id.org/security/v1",
587
+ "https://w3id.org/security/data-integrity/v1"
588
+ ],
589
+ id: "https://example.com/activities/property-scoped-contexts",
590
+ type: "Create",
591
+ actor: "https://example.com/person2",
592
+ "https://example.com/p": { "https://example.com/nested": "value" },
593
+ object: "https://example.com/notes/1"
594
+ });
595
+ assert(calls.includes(rootContextId));
596
+ assert(calls.includes(childContextUrl));
597
+ assertFalse(calls.includes("./child"));
598
+ });
599
+ test("compactJsonLd() ignores unsafe-looking keys inside @json values", async () => {
600
+ const remoteContextUrl = "https://example.com/contexts/json";
601
+ assertEquals(await compactJsonLd({
602
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
603
+ id: "https://example.com/activities/json-blob",
604
+ type: "Create",
605
+ actor: "https://example.com/person2",
606
+ blob: {
607
+ graph: { nested: true },
608
+ "@reverse": { nope: true },
609
+ "@included": [{ still: "raw-json" }]
610
+ },
611
+ object: "https://example.com/notes/1"
612
+ }, async (resource) => {
613
+ const url = new URL(resource).href;
614
+ if (url === remoteContextUrl) return {
615
+ contextUrl: null,
616
+ documentUrl: url,
617
+ document: { "@context": {
618
+ blob: {
619
+ "@id": "https://example.com/blob",
620
+ "@type": "@json"
621
+ },
622
+ graph: "@graph"
623
+ } }
624
+ };
625
+ return await mockDocumentLoader(url);
626
+ }), {
627
+ "@context": [
628
+ "https://w3id.org/identity/v1",
629
+ "https://www.w3.org/ns/activitystreams",
630
+ "https://w3id.org/security/v1",
631
+ "https://w3id.org/security/data-integrity/v1"
632
+ ],
633
+ id: "https://example.com/activities/json-blob",
634
+ type: "Create",
635
+ "https://example.com/blob": {
636
+ type: "@json",
637
+ "@value": {
638
+ graph: { nested: true },
639
+ "@reverse": { nope: true },
640
+ "@included": [{ still: "raw-json" }]
641
+ }
642
+ },
643
+ actor: "https://example.com/person2",
644
+ object: "https://example.com/notes/1"
645
+ });
646
+ });
647
+ test("compactJsonLd() ignores unsafe-looking keys inside inline @json value wrappers", async () => {
648
+ const remoteContextUrl = "https://example.com/contexts/inline-json";
649
+ assertEquals(await compactJsonLd({
650
+ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
651
+ id: "https://example.com/activities/inline-json-blob",
652
+ type: "Create",
653
+ actor: "https://example.com/person2",
654
+ blob: {
655
+ "@value": {
656
+ graph: { nested: true },
657
+ "@reverse": { nope: true },
658
+ "@included": [{ still: "raw-json" }]
659
+ },
660
+ "@type": "@json"
661
+ },
662
+ object: "https://example.com/notes/1"
663
+ }, async (resource) => {
664
+ const url = new URL(resource).href;
665
+ if (url === remoteContextUrl) return {
666
+ contextUrl: null,
667
+ documentUrl: url,
668
+ document: { "@context": {
669
+ blob: "https://example.com/blob",
670
+ graph: "@graph"
671
+ } }
672
+ };
673
+ return await mockDocumentLoader(url);
674
+ }), {
675
+ "@context": [
676
+ "https://w3id.org/identity/v1",
677
+ "https://www.w3.org/ns/activitystreams",
678
+ "https://w3id.org/security/v1",
679
+ "https://w3id.org/security/data-integrity/v1"
680
+ ],
681
+ id: "https://example.com/activities/inline-json-blob",
682
+ type: "Create",
683
+ "https://example.com/blob": {
684
+ type: "@json",
685
+ "@value": {
686
+ graph: { nested: true },
687
+ "@reverse": { nope: true },
688
+ "@included": [{ still: "raw-json" }]
689
+ }
690
+ },
691
+ actor: "https://example.com/person2",
692
+ object: "https://example.com/notes/1"
693
+ });
694
+ });
695
+ test("verifyJsonLd() respects @graph alias overrides", async () => {
696
+ assert(await verifyJsonLd(await signJsonLd({
697
+ "@context": [
698
+ "https://www.w3.org/ns/activitystreams",
699
+ { graph: "@graph" },
700
+ { graph: "https://example.com/graph" }
701
+ ],
702
+ id: "https://example.com/activities/1",
703
+ type: "Create",
704
+ actor: "https://example.com/person2",
705
+ object: "https://example.com/notes/1",
706
+ graph: "https://example.com/custom-graph"
707
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }), {
708
+ documentLoader: mockDocumentLoader,
709
+ contextLoader: mockDocumentLoader
710
+ }));
711
+ });
712
+ test("compactJsonLd() respects nested @context scope for @graph aliases", async () => {
713
+ assertEquals(await compactJsonLd({
714
+ "@context": ["https://www.w3.org/ns/activitystreams", {
715
+ graph: "https://example.com/graph",
716
+ meta: {
717
+ "@id": "https://example.com/meta",
718
+ "@context": { graph: "@graph" }
719
+ }
720
+ }],
721
+ id: "https://example.com/activities/2",
722
+ type: "Create",
723
+ actor: "https://example.com/person2",
724
+ object: "https://example.com/notes/2",
725
+ graph: "https://example.com/custom-graph",
726
+ meta: { value: "ok" }
727
+ }, mockDocumentLoader), {
728
+ "@context": [
729
+ "https://w3id.org/identity/v1",
730
+ "https://www.w3.org/ns/activitystreams",
731
+ "https://w3id.org/security/v1",
732
+ "https://w3id.org/security/data-integrity/v1"
733
+ ],
734
+ id: "https://example.com/activities/2",
735
+ type: "Create",
736
+ "https://example.com/graph": "https://example.com/custom-graph",
737
+ actor: "https://example.com/person2",
738
+ object: "https://example.com/notes/2",
739
+ "https://example.com/meta": { value: "ok" }
740
+ });
741
+ });
742
+ test("compactJsonLd() resets inherited @graph aliases on @context: null", async () => {
743
+ assertEquals(await compactJsonLd({
744
+ "@context": ["https://www.w3.org/ns/activitystreams", { g: "@graph" }],
745
+ id: "https://example.com/activities/3",
746
+ type: "Create",
747
+ actor: "https://example.com/person2",
748
+ object: {
749
+ "@context": null,
750
+ g: "literal"
751
+ }
752
+ }, mockDocumentLoader), {
753
+ "@context": [
754
+ "https://w3id.org/identity/v1",
755
+ "https://www.w3.org/ns/activitystreams",
756
+ "https://w3id.org/security/v1",
757
+ "https://w3id.org/security/data-integrity/v1"
758
+ ],
759
+ id: "https://example.com/activities/3",
760
+ type: "Create",
761
+ actor: "https://example.com/person2",
762
+ object: {}
763
+ });
764
+ });
765
+ test("compactJsonLd() rejects same-object forward @graph alias chains", async () => {
766
+ await assertRejects(() => compactJsonLd({
767
+ "@context": ["https://www.w3.org/ns/activitystreams", {
768
+ a: "b",
769
+ b: "@graph"
770
+ }],
771
+ id: "https://example.com/activities/forward-graph-alias",
772
+ type: "Create",
773
+ actor: "https://example.com/person2",
774
+ a: [{
775
+ id: "https://example.com/notes/forward-graph-alias",
776
+ type: "Note",
777
+ content: "Hello"
778
+ }]
779
+ }, mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
780
+ });
781
+ test("compactJsonLd() preserves captured @graph aliases across later overrides", async () => {
782
+ await assertRejects(() => compactJsonLd({
783
+ "@context": [
784
+ "https://www.w3.org/ns/activitystreams",
785
+ { b: "@graph" },
786
+ { a: "b" },
787
+ { b: "https://example.com/b" }
788
+ ],
789
+ id: "https://example.com/activities/captured-graph-alias",
790
+ type: "Create",
791
+ actor: "https://example.com/person2",
792
+ a: [{
793
+ id: "https://example.com/notes/captured-graph-alias",
794
+ type: "Note",
795
+ content: "Hello"
796
+ }]
797
+ }, mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
798
+ });
799
+ test("compactJsonLd() does not retroactively apply later @graph aliases", async () => {
800
+ assertEquals(await compactJsonLd({
801
+ "@context": [
802
+ "https://www.w3.org/ns/activitystreams",
803
+ { a: "b" },
804
+ { b: "@graph" }
805
+ ],
806
+ id: "https://example.com/activities/non-retroactive-graph-alias",
807
+ type: "Create",
808
+ actor: "https://example.com/person2",
809
+ a: [{
810
+ id: "https://example.com/notes/non-retroactive-graph-alias",
811
+ type: "Note",
812
+ content: "Hello"
813
+ }]
814
+ }, mockDocumentLoader), {
815
+ "@context": [
816
+ "https://w3id.org/identity/v1",
817
+ "https://www.w3.org/ns/activitystreams",
818
+ "https://w3id.org/security/v1",
819
+ "https://w3id.org/security/data-integrity/v1"
820
+ ],
821
+ id: "https://example.com/activities/non-retroactive-graph-alias",
822
+ type: "Create",
823
+ b: {
824
+ id: "https://example.com/notes/non-retroactive-graph-alias",
825
+ type: "Note",
826
+ content: "Hello"
827
+ },
828
+ actor: "https://example.com/person2"
829
+ });
830
+ });
831
+ test("verifyJsonLd() rejects unsafe JSON-LD keywords", async () => {
832
+ const original = {
833
+ "@context": "https://www.w3.org/ns/activitystreams",
834
+ id: "https://example.com/activities/undo",
835
+ type: "Undo",
836
+ actor: "https://example.com/person2",
837
+ object: {
838
+ id: "https://example.com/activities/announce",
839
+ type: "Announce",
840
+ actor: "https://example.com/person2",
841
+ object: "https://example.com/status/1"
842
+ }
843
+ };
844
+ const signed = await signJsonLd(original, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
845
+ const options = {
846
+ documentLoader: mockDocumentLoader,
847
+ contextLoader: mockDocumentLoader
848
+ };
849
+ const cases = [
850
+ ["@reverse", {
851
+ "@context": ["https://www.w3.org/ns/activitystreams", { rev: "@reverse" }],
852
+ id: "https://example.com/activities/announce",
853
+ type: "Announce",
854
+ actor: "https://example.com/person2",
855
+ object: "https://example.com/status/1",
856
+ rev: { object: {
857
+ id: "https://example.com/activities/undo",
858
+ type: "Undo",
859
+ actor: "https://example.com/person2"
860
+ } },
861
+ signature: signed.signature
862
+ }],
863
+ ["@included", {
864
+ "@context": ["https://www.w3.org/ns/activitystreams", { inc: "@included" }],
865
+ id: "https://example.com/activities/announce",
866
+ type: "Announce",
867
+ actor: "https://example.com/person2",
868
+ object: "https://example.com/status/1",
869
+ inc: [{
870
+ id: "https://example.com/activities/undo",
871
+ type: "Undo",
872
+ actor: "https://example.com/person2",
873
+ object: "https://example.com/activities/announce"
874
+ }],
875
+ signature: signed.signature
876
+ }],
877
+ ["@graph", {
878
+ "@context": ["https://www.w3.org/ns/activitystreams", { graph: "@graph" }],
879
+ graph: [original],
880
+ signature: signed.signature
881
+ }],
882
+ ["@graph", {
883
+ "@context": ["https://www.w3.org/ns/activitystreams", { graph: "@graph" }],
884
+ id: "https://example.com/activities/announce",
885
+ type: "Announce",
886
+ actor: "https://example.com/person2",
887
+ object: "https://example.com/status/1",
888
+ graph: [{
889
+ id: "https://example.com/activities/undo",
890
+ type: "Undo",
891
+ actor: "https://example.com/person2",
892
+ object: "https://example.com/activities/announce"
893
+ }],
894
+ signature: signed.signature
895
+ }]
896
+ ];
897
+ for (const [keyword, jsonLd] of cases) await assertRejects(() => verifyJsonLd(jsonLd, options), UnsafeJsonLdError, `Unsupported JSON-LD keyword: ${keyword}.`);
898
+ });
899
+ test("compactJsonLd() rejects unsafe JSON-LD keywords inside signature objects", async () => {
900
+ const signed = await signJsonLd({
901
+ "@context": "https://www.w3.org/ns/activitystreams",
902
+ id: "https://example.com/activities/signed-signature-keywords",
903
+ type: "Create",
904
+ actor: "https://example.com/person2",
905
+ object: "https://example.com/notes/1"
906
+ }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
907
+ for (const [keyword, value] of [
908
+ ["@reverse", { object: {
909
+ id: "https://example.com/activities/reverse-inside-signature",
910
+ type: "Undo"
911
+ } }],
912
+ ["@included", [{
913
+ id: "https://example.com/activities/included-inside-signature",
914
+ type: "Undo"
915
+ }]],
916
+ ["@graph", [{
917
+ id: "https://example.com/activities/graph-inside-signature",
918
+ type: "Undo"
919
+ }]]
920
+ ]) await assertRejects(() => compactJsonLd({
921
+ ...signed,
922
+ signature: {
923
+ ...signed.signature,
924
+ [keyword]: value
925
+ }
926
+ }, mockDocumentLoader), UnsafeJsonLdError, `Unsupported JSON-LD keyword: ${keyword}.`);
927
+ });
928
+ test("compactJsonLd() rejects inputs that compact into @graph wrappers", async () => {
929
+ await assertRejects(() => compactJsonLd([{
930
+ "@context": "https://www.w3.org/ns/activitystreams",
931
+ id: "https://example.com/notes/graph-wrapper-1",
932
+ type: "Note",
933
+ content: "one"
934
+ }, {
935
+ "@context": "https://www.w3.org/ns/activitystreams",
936
+ id: "https://example.com/notes/graph-wrapper-2",
937
+ type: "Note",
938
+ content: "two"
939
+ }], mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
940
+ });
252
941
  //#endregion
253
942
  export {};