@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2492

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 (141) hide show
  1. package/README.md +3 -0
  2. package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
  3. package/dist/{builder-Da8b9O6v.mjs → builder-DphK9v7f.mjs} +5 -5
  4. package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.cts} +0 -1
  5. package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.ts} +0 -1
  6. package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
  7. package/dist/compat/mod.d.cts +1 -1
  8. package/dist/compat/mod.d.ts +1 -1
  9. package/dist/compat/transformers.test.mjs +1 -1
  10. package/dist/{compound-proof-D1djO5fa.mjs → compound-proof-cYtvlTgF.mjs} +2 -2
  11. package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
  12. package/dist/{context-BdB54V-A.d.ts → context-DQJE9YfH.d.ts} +332 -6
  13. package/dist/{context-CQ9H8vNW.d.cts → context-DSuL3YWx.d.cts} +332 -6
  14. package/dist/{deno-eNpfj4oR.mjs → deno-iOAA7iRN.mjs} +1 -1
  15. package/dist/docloader-DCZkiTeF.mjs +136 -0
  16. package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
  17. package/dist/federation/builder.test.mjs +1 -1
  18. package/dist/federation/compound-fanout.test.mjs +6 -3
  19. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  20. package/dist/federation/handler.test.mjs +22 -7
  21. package/dist/federation/hashlink-media.test.mjs +2 -2
  22. package/dist/federation/idempotency.test.mjs +2 -2
  23. package/dist/federation/inbox-actorless.test.d.mts +2 -0
  24. package/dist/federation/inbox-actorless.test.mjs +487 -0
  25. package/dist/federation/inbox-authentication.test.d.mts +2 -0
  26. package/dist/federation/inbox-authentication.test.mjs +116 -0
  27. package/dist/federation/inbox-report.test.mjs +5 -5
  28. package/dist/federation/inbox-view.test.d.mts +2 -0
  29. package/dist/federation/inbox-view.test.mjs +369 -0
  30. package/dist/federation/inbox.test.mjs +22 -12
  31. package/dist/federation/metrics.test.mjs +1 -1
  32. package/dist/federation/middleware.test.mjs +470 -35
  33. package/dist/federation/mod.cjs +2 -1
  34. package/dist/federation/mod.d.cts +4 -4
  35. package/dist/federation/mod.d.ts +4 -4
  36. package/dist/federation/mod.js +8 -2
  37. package/dist/federation/mq.test.mjs +36 -1
  38. package/dist/federation/outgoing-proof.test.mjs +9 -6
  39. package/dist/federation/portable-actor.test.mjs +3 -3
  40. package/dist/federation/portable-chain.test.mjs +3 -3
  41. package/dist/federation/portable-collection.test.mjs +4 -4
  42. package/dist/federation/portable-delivery.test.mjs +2 -2
  43. package/dist/federation/portable-gateway-keys.test.mjs +13 -7
  44. package/dist/federation/portable-inbox.test.mjs +4 -4
  45. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  46. package/dist/federation/portable-route-activity.test.mjs +2 -2
  47. package/dist/federation/portable.test.mjs +3 -3
  48. package/dist/federation/send.test.mjs +502 -93
  49. package/dist/federation/tasks/enqueue.test.mjs +3 -3
  50. package/dist/federation/tasks/tasks.test.mjs +4 -4
  51. package/dist/federation/temporal.test.mjs +1 -1
  52. package/dist/federation/webfinger.test.mjs +2 -2
  53. package/dist/{federation-BVH7drJA.cjs → federation-D0AZw_aw.cjs} +1415 -236
  54. package/dist/{http-BUJ5SFh6.mjs → http-BoJCMnjY.mjs} +204 -106
  55. package/dist/{http-DlNxxW_5.js → http-ChXUU_3c.js} +319 -123
  56. package/dist/{http-BfW3wyQ4.cjs → http-kv1LZw-r.cjs} +339 -125
  57. package/dist/inbox-BjihTKLo.mjs +312 -0
  58. package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
  59. package/dist/inbox-view-D-skypg-.mjs +385 -0
  60. package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
  61. package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
  62. package/dist/{kv-cache-D_m3A241.mjs → kv-cache-Cmsyke4d.mjs} +1 -1
  63. package/dist/{kv-cache-7015lbrx.js → kv-cache-DQcte7ES.js} +73 -28
  64. package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-Ddh5sMxH.cjs} +72 -27
  65. package/dist/ld-CWVpo_NM.d.cts +250 -0
  66. package/dist/{ld-CD2CsPxq.mjs → ld-CdvcOEXQ.mjs} +52 -9
  67. package/dist/ld-Inagww4R.d.ts +250 -0
  68. package/dist/{metrics-D3LHk6Z4.mjs → metrics-CpxvUrBz.mjs} +1 -1
  69. package/dist/{middleware-N9eM16XE.js → middleware-BHb5toqA.js} +1413 -247
  70. package/dist/{middleware-CCDXvXOG.mjs → middleware-BKs2DLUF.mjs} +1 -1
  71. package/dist/{middleware-CzoWvrdZ.mjs → middleware-CoBLrTKX.mjs} +533 -132
  72. package/dist/{mod-B0dGgQsg.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
  73. package/dist/mod-B4kBw1-8.d.ts +154 -0
  74. package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
  75. package/dist/mod-C1nT74IG.d.cts +154 -0
  76. package/dist/{mod-D0bxB8jS.d.ts → mod-CIeBgsOC.d.ts} +1 -1
  77. package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
  78. package/dist/mod.cjs +6 -5
  79. package/dist/mod.d.cts +9 -9
  80. package/dist/mod.d.ts +9 -9
  81. package/dist/mod.js +6 -6
  82. package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
  83. package/dist/nodeinfo/client.test.mjs +337 -19
  84. package/dist/nodeinfo/handler.test.mjs +2 -2
  85. package/dist/nodeinfo/mod.cjs +1 -1
  86. package/dist/nodeinfo/mod.d.cts +1 -1
  87. package/dist/nodeinfo/mod.d.ts +1 -1
  88. package/dist/nodeinfo/mod.js +1 -1
  89. package/dist/otel/exporter.test.mjs +257 -1
  90. package/dist/otel/mod.cjs +36 -30
  91. package/dist/otel/mod.d.cts +13 -0
  92. package/dist/otel/mod.d.ts +13 -0
  93. package/dist/otel/mod.js +36 -30
  94. package/dist/{owner-C5-FVX6t.mjs → owner-DTnVpUKA.mjs} +3 -3
  95. package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CJeYTL61.mjs} +2 -2
  96. package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-DciwBIia.cjs} +1 -1
  97. package/dist/{portable-collection-CC2zNAje.js → portable-collection-TmtpzD4E.js} +1 -1
  98. package/dist/{portable-key-DNc_flH-.mjs → portable-key-DcYok4ki.mjs} +1 -1
  99. package/dist/{proof-Cryb1c70.mjs → proof-GH7Poos_.mjs} +98 -10
  100. package/dist/{send-AF25pWaJ.mjs → send-BgGJh6pv.mjs} +174 -76
  101. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  102. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  103. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  104. package/dist/sig/compound-proof.test.mjs +2 -2
  105. package/dist/sig/http.test.mjs +553 -2
  106. package/dist/sig/key.test.mjs +1 -1
  107. package/dist/sig/ld.test.mjs +1 -1
  108. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  109. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  110. package/dist/sig/mod.cjs +2 -2
  111. package/dist/sig/mod.d.cts +3 -3
  112. package/dist/sig/mod.d.ts +3 -3
  113. package/dist/sig/mod.js +2 -2
  114. package/dist/sig/owner.test.mjs +1 -1
  115. package/dist/sig/portable-collection.test.mjs +1 -1
  116. package/dist/sig/portable-interop.test.mjs +3 -3
  117. package/dist/sig/portable-key.test.mjs +5 -5
  118. package/dist/sig/proof.test.mjs +45 -5
  119. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  120. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  121. package/dist/sig/verification.test.mjs +3 -3
  122. package/dist/{temporal-BsAeuUJj.mjs → temporal-gJINzcL4.mjs} +1 -1
  123. package/dist/testing/kv.test.mjs +1 -1
  124. package/dist/testing/mod.d.mts +288 -2
  125. package/dist/testing/mod.mjs +2 -2
  126. package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
  127. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  128. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  129. package/dist/utils/docloader.test.mjs +344 -36
  130. package/dist/utils/kv-cache.test.mjs +1 -1
  131. package/dist/utils/mod.cjs +1 -1
  132. package/dist/utils/mod.d.cts +1 -1
  133. package/dist/utils/mod.d.ts +1 -1
  134. package/dist/utils/mod.js +1 -1
  135. package/package.json +7 -7
  136. package/dist/docloader-B-0nqOB1.mjs +0 -91
  137. package/dist/inbox-CHvMaIFX.mjs +0 -188
  138. package/dist/mod-BRpER-B0.d.ts +0 -320
  139. package/dist/mod-xAfaLS4T.d.cts +0 -320
  140. package/dist/owner-BF0L9cMt.d.cts +0 -78
  141. package/dist/owner-BSzr5-I8.d.ts +0 -78
@@ -0,0 +1,385 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { c as getNormalizationContextLoader, p as markEffectiveContextLoader, s as detachSignature, y as wrapContextLoaderForJsonLd } from "./ld-CdvcOEXQ.mjs";
5
+ import { preloadedContexts } from "@fedify/vocab-runtime";
6
+ import { encodeHex } from "byte-encodings/hex";
7
+ import jsonld from "@fedify/vocab-runtime/jsonld";
8
+ import serialize from "json-canon";
9
+ //#region src/federation/inbox-view.ts
10
+ /**
11
+ * The interpretation of an incoming activity that Fedify authenticated, kept
12
+ * so that listeners, authorization helpers, and queue workers observe the
13
+ * same JSON-LD meaning that signature verification observed.
14
+ *
15
+ * Every JSON-LD processing step of one delivery goes through a request-scoped
16
+ * *context snapshot*, which memoizes the remote context responses so that
17
+ * a nondeterministic or mutable remote context cannot give verification and
18
+ * dispatch different meanings. The snapshot is serialized into queued
19
+ * messages, so that workers and retries replay the very same responses
20
+ * without loading contexts again.
21
+ * @module
22
+ */
23
+ /**
24
+ * Thrown when replaying an interpretation needs a context response that its
25
+ * snapshot does not have, or has with a different content.
26
+ * @internal
27
+ */
28
+ var ContextSnapshotMissError = class extends Error {
29
+ url;
30
+ constructor(url, reason) {
31
+ super(`Context ${url} cannot be replayed: ${reason}.`);
32
+ this.name = "ContextSnapshotMissError";
33
+ this.url = url;
34
+ }
35
+ };
36
+ function normalizeUrl(url) {
37
+ return URL.canParse(url) ? new URL(url).href : url;
38
+ }
39
+ /**
40
+ * Creates the context snapshot of a delivery.
41
+ * @param base The caller's context loader.
42
+ * @returns The snapshot.
43
+ * @internal
44
+ */
45
+ function createContextSnapshot(base) {
46
+ const underlying = {
47
+ standard: wrapContextLoaderForJsonLd(base),
48
+ normalization: getNormalizationContextLoader(base)
49
+ };
50
+ const cache = /* @__PURE__ */ new Map();
51
+ const recorded = /* @__PURE__ */ new Map();
52
+ const loaders = /* @__PURE__ */ new Map();
53
+ let released = false;
54
+ return {
55
+ loader(policy) {
56
+ let loader = loaders.get(policy);
57
+ if (loader != null) return loader;
58
+ const effective = underlying[policy];
59
+ loader = markEffectiveContextLoader(async (url, options) => {
60
+ if (released) return await effective(url, options);
61
+ const normalizedUrl = normalizeUrl(url);
62
+ const key = `${policy}\n${normalizedUrl}`;
63
+ let promise = cache.get(key);
64
+ if (promise == null) {
65
+ promise = Promise.resolve(effective(url, options)).then((response) => {
66
+ const cloned = normalizeResponse(structuredClone(response));
67
+ recorded.set(key, {
68
+ policy,
69
+ url: normalizedUrl,
70
+ response: structuredClone(cloned)
71
+ });
72
+ return cloned;
73
+ });
74
+ promise.catch(() => {});
75
+ cache.set(key, promise);
76
+ }
77
+ return structuredClone(await promise);
78
+ }, policy);
79
+ loaders.set(policy, loader);
80
+ return loader;
81
+ },
82
+ entries() {
83
+ return [...recorded.values()];
84
+ },
85
+ release() {
86
+ released = true;
87
+ }
88
+ };
89
+ }
90
+ function getBundledResponse(url) {
91
+ if (!(url in preloadedContexts)) return null;
92
+ return {
93
+ contextUrl: null,
94
+ document: preloadedContexts[url],
95
+ documentUrl: url
96
+ };
97
+ }
98
+ async function sha256(value) {
99
+ return encodeHex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialize(value))));
100
+ }
101
+ function normalizeResponse(response) {
102
+ return {
103
+ contextUrl: response.contextUrl ?? null,
104
+ document: response.document,
105
+ documentUrl: response.documentUrl
106
+ };
107
+ }
108
+ /**
109
+ * Serializes the entries of a context snapshot for a queued message.
110
+ * @param entries The entries.
111
+ * @returns The serialized entries, which are plain JSON values.
112
+ * @internal
113
+ */
114
+ async function serializeContextSnapshot(entries) {
115
+ const result = [];
116
+ for (const entry of entries) {
117
+ const response = normalizeResponse(entry.response);
118
+ const hash = await sha256(response);
119
+ const bundled = getBundledResponse(entry.url);
120
+ if (bundled != null && await sha256(bundled) === hash) result.push({
121
+ policy: entry.policy,
122
+ url: entry.url,
123
+ sha256: hash,
124
+ bundled: true
125
+ });
126
+ else result.push({
127
+ policy: entry.policy,
128
+ url: entry.url,
129
+ sha256: hash,
130
+ response: structuredClone(response)
131
+ });
132
+ }
133
+ return result;
134
+ }
135
+ function isRemoteDocument(value) {
136
+ if (value == null || typeof value !== "object") return false;
137
+ const record = value;
138
+ return typeof record.documentUrl === "string" && "document" in record && (record.contextUrl == null || typeof record.contextUrl === "string");
139
+ }
140
+ /**
141
+ * Restores the entries of a serialized context snapshot, verifying the
142
+ * hashes of stored responses. Bundled entries are reconstructed from the
143
+ * contexts this version of Fedify bundles.
144
+ * @param entries The serialized entries.
145
+ * @returns The entries.
146
+ * @throws {ContextSnapshotMissError} If an entry is malformed, or its
147
+ * content does not match its hash.
148
+ * @internal
149
+ */
150
+ async function deserializeContextSnapshot(entries) {
151
+ if (!Array.isArray(entries)) throw new ContextSnapshotMissError("(snapshot)", "malformed snapshot");
152
+ const result = [];
153
+ for (const entry of entries) {
154
+ if (entry == null || typeof entry !== "object" || entry.policy !== "standard" && entry.policy !== "normalization" || typeof entry.url !== "string" || typeof entry.sha256 !== "string") throw new ContextSnapshotMissError("(snapshot)", "malformed entry");
155
+ let response;
156
+ if (entry.bundled === true) {
157
+ response = getBundledResponse(entry.url);
158
+ if (response == null) throw new ContextSnapshotMissError(entry.url, "not bundled");
159
+ } else if (isRemoteDocument(entry.response)) response = normalizeResponse(entry.response);
160
+ else throw new ContextSnapshotMissError(entry.url, "malformed entry");
161
+ if (entry.bundled !== true && await sha256(response) !== entry.sha256) throw new ContextSnapshotMissError(entry.url, "content mismatch");
162
+ result.push({
163
+ policy: entry.policy,
164
+ url: entry.url,
165
+ response: structuredClone(response)
166
+ });
167
+ }
168
+ return result;
169
+ }
170
+ /**
171
+ * Creates a context loader of a policy that serves only the responses of
172
+ * a snapshot. It is marked as the effective loader of the policy.
173
+ * @param entries The snapshot's entries.
174
+ * @param policy The policy.
175
+ * @returns The loader, which throws {@link ContextSnapshotMissError} for
176
+ * anything else.
177
+ * @internal
178
+ */
179
+ function createSnapshotOnlyLoader(entries, policy) {
180
+ const responses = /* @__PURE__ */ new Map();
181
+ for (const entry of entries) if (entry.policy === policy) responses.set(entry.url, entry.response);
182
+ return markEffectiveContextLoader(async (url) => {
183
+ const response = responses.get(normalizeUrl(url));
184
+ if (response == null) throw new ContextSnapshotMissError(url, "not in the snapshot");
185
+ return structuredClone(response);
186
+ }, policy);
187
+ }
188
+ /**
189
+ * Creates a {@link ReleasableLoader}.
190
+ * @param strict The loader to use until release.
191
+ * @param fallback The loader to use after release.
192
+ * @param policy The policy both loaders apply.
193
+ * @returns The releasable loader.
194
+ * @internal
195
+ */
196
+ function createReleasableLoader(strict, fallback, policy) {
197
+ let released = false;
198
+ return {
199
+ loader: markEffectiveContextLoader((url, options) => released ? fallback(url, options) : strict(url, options), policy),
200
+ release() {
201
+ released = true;
202
+ }
203
+ };
204
+ }
205
+ /**
206
+ * Expands a JSON-LD document with the given context loader.
207
+ * @param document The document.
208
+ * @param contextLoader The context loader.
209
+ * @returns The expanded document.
210
+ * @internal
211
+ */
212
+ async function expandDocument(document, contextLoader) {
213
+ return await jsonld.expand(document, { documentLoader: contextLoader });
214
+ }
215
+ /**
216
+ * Computes the digest of an expanded document.
217
+ * @param expanded The expanded document.
218
+ * @returns The hex-encoded SHA-256 of its canonical JSON.
219
+ * @internal
220
+ */
221
+ function digestExpandedDocument(expanded) {
222
+ return sha256(expanded);
223
+ }
224
+ /**
225
+ * Deeply freezes a JSON value.
226
+ * @param value The value.
227
+ * @returns The same value, frozen.
228
+ * @internal
229
+ */
230
+ function deepFreeze(value) {
231
+ if (value != null && typeof value === "object" && !Object.isFrozen(value)) {
232
+ Object.freeze(value);
233
+ for (const child of Object.values(value)) deepFreeze(child);
234
+ }
235
+ return value;
236
+ }
237
+ const ACTOR_IRI = "https://www.w3.org/ns/activitystreams#actor";
238
+ /**
239
+ * Gets the single root node of an expanded document.
240
+ * @param expanded The expanded document.
241
+ * @returns The root node, or `null` if there is not exactly one.
242
+ * @internal
243
+ */
244
+ function getRootNode(expanded) {
245
+ if (expanded.length !== 1) return null;
246
+ const node = expanded[0];
247
+ if (node == null || typeof node !== "object" || Array.isArray(node)) return null;
248
+ return node;
249
+ }
250
+ /**
251
+ * Gets the values of a property of an expanded node, unwrapping a single
252
+ * `@list` container the way generated vocabulary decoders do.
253
+ * @param node The expanded node.
254
+ * @param iri The property IRI.
255
+ * @returns The values, or an empty array if the property is absent.
256
+ * @internal
257
+ */
258
+ function getPropertyValues(node, iri) {
259
+ const values = node[iri];
260
+ if (values == null) return [];
261
+ if (!Array.isArray(values)) return [values];
262
+ if (values.length === 1 && values[0] != null && typeof values[0] === "object" && "@list" in values[0]) {
263
+ const list = values[0]["@list"];
264
+ return Array.isArray(list) ? list : [list];
265
+ }
266
+ return values;
267
+ }
268
+ /**
269
+ * Tells whether the root node of an expanded activity declares any `actor`
270
+ * value at all, including blank nodes, anonymous or untyped nodes, and
271
+ * literals, which ID accessors and typed parsing may drop.
272
+ * @param expanded The expanded activity.
273
+ * @returns `"absent"` if the root node declares no actor; `"present"`
274
+ * otherwise, including when there is no single root node.
275
+ * @internal
276
+ */
277
+ function getActorPresence(expanded) {
278
+ const root = getRootNode(expanded);
279
+ if (root == null) return "present";
280
+ return getPropertyValues(root, ACTOR_IRI).length < 1 ? "absent" : "present";
281
+ }
282
+ /**
283
+ * Builds the serialized view of an authenticated activity. The document is
284
+ * expanded through the snapshot, so that the snapshot holds every context
285
+ * response the interpretation needs.
286
+ * @param snapshot The delivery's context snapshot.
287
+ * @param source Which document the activity was parsed from.
288
+ * @returns The serialized view and the expanded activity.
289
+ * @internal
290
+ */
291
+ async function buildVerifiedActivityView(snapshot, source) {
292
+ const loader = snapshot.loader(source.policy);
293
+ const used = /* @__PURE__ */ new Set();
294
+ const expanded = await expandDocument(source.document, markEffectiveContextLoader((url, options) => {
295
+ used.add(normalizeUrl(url));
296
+ return loader(url, options);
297
+ }, source.policy));
298
+ const entries = snapshot.entries().filter((entry) => entry.policy === source.policy && used.has(entry.url));
299
+ const contexts = await serializeContextSnapshot(entries);
300
+ const references = /* @__PURE__ */ new Set();
301
+ collectContextReferences(source.document, source.baseUrl?.href, references);
302
+ for (const entry of entries) {
303
+ const { contextUrl, document } = entry.response;
304
+ collectContextReferences(document, entry.url, references);
305
+ if (contextUrl != null) collectContextReferences({ "@context": contextUrl }, entry.url, references);
306
+ }
307
+ return {
308
+ replayable: [...references].every((url) => used.has(url)),
309
+ view: {
310
+ version: 1,
311
+ policy: source.policy,
312
+ source: source.source,
313
+ baseUrl: source.baseUrl?.href ?? null,
314
+ contexts,
315
+ expandedDigest: await digestExpandedDocument(expanded)
316
+ },
317
+ expanded
318
+ };
319
+ }
320
+ /**
321
+ * Collects the remote context references of a JSON-LD document or context
322
+ * document: every IRI given as a value of `@context` at any depth, including
323
+ * scoped contexts of term definitions, and of `@import`.
324
+ * @param value The document.
325
+ * @param base The URL to resolve relative references against.
326
+ * @param references The set to add the normalized references to.
327
+ */
328
+ function collectContextReferences(value, base, references) {
329
+ const add = (reference) => {
330
+ let url = reference;
331
+ if (base != null && URL.canParse(reference, base)) url = new URL(reference, base).href;
332
+ references.add(normalizeUrl(url));
333
+ };
334
+ const collectContext = (context) => {
335
+ if (typeof context === "string") add(context);
336
+ else if (Array.isArray(context)) for (const item of context) collectContext(item);
337
+ else if (context != null && typeof context === "object") {
338
+ const imported = context["@import"];
339
+ if (typeof imported === "string") add(imported);
340
+ walk(context);
341
+ }
342
+ };
343
+ const walk = (node) => {
344
+ if (Array.isArray(node)) for (const item of node) walk(item);
345
+ else if (node != null && typeof node === "object") for (const [key, item] of Object.entries(node)) if (key === "@context") collectContext(item);
346
+ else walk(item);
347
+ };
348
+ walk(value);
349
+ }
350
+ /**
351
+ * Validates the shape of a serialized view taken from a queued message.
352
+ * @param value The value of the message's `verifiedView` field.
353
+ * @returns The view, or `null` if it is malformed or of an unsupported
354
+ * version.
355
+ * @internal
356
+ */
357
+ function parseSerializedVerifiedActivityView(value) {
358
+ if (value == null || typeof value !== "object") return null;
359
+ const view = value;
360
+ if (view.version !== 1 || view.policy !== "standard" && view.policy !== "normalization" || view.source !== "activity" && view.source !== "normalizedActivity" || view.baseUrl !== null && (typeof view.baseUrl !== "string" || !URL.canParse(view.baseUrl)) || !Array.isArray(view.contexts) || typeof view.expandedDigest !== "string") return null;
361
+ return view;
362
+ }
363
+ /**
364
+ * Restores a view from a queued message without loading any context.
365
+ * @param view The serialized view.
366
+ * @param message The fields of the message the view refers to.
367
+ * @returns The restored view.
368
+ * @throws {ContextSnapshotMissError} If the view cannot be replayed exactly.
369
+ * @internal
370
+ */
371
+ async function restoreVerifiedActivityView(view, message) {
372
+ const loader = createSnapshotOnlyLoader(await deserializeContextSnapshot(view.contexts), view.policy);
373
+ if (view.source === "normalizedActivity" && message.normalizedActivity == null) throw new ContextSnapshotMissError("(normalizedActivity)", "the normalized activity is missing");
374
+ const document = detachSignature(view.source === "activity" ? message.activity : message.normalizedActivity);
375
+ const expanded = await expandDocument(document, loader);
376
+ if (await digestExpandedDocument(expanded) !== view.expandedDigest) throw new ContextSnapshotMissError("(activity)", "the interpretation does not match the authenticated one");
377
+ return {
378
+ document,
379
+ loader,
380
+ expanded,
381
+ baseUrl: view.baseUrl == null ? null : new URL(view.baseUrl)
382
+ };
383
+ }
384
+ //#endregion
385
+ export { createSnapshotOnlyLoader as a, expandDocument as c, parseSerializedVerifiedActivityView as d, restoreVerifiedActivityView as f, createReleasableLoader as i, getActorPresence as l, buildVerifiedActivityView as n, deepFreeze as o, serializeContextSnapshot as p, createContextSnapshot as r, deserializeContextSnapshot as s, ContextSnapshotMissError as t, getPropertyValues as u };
@@ -1,8 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Actor, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1 } from "@fedify/vocab";
3
- import { DocumentLoader, PortableObjectVerifier } from "@fedify/vocab-runtime";
4
3
  import { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
-
4
+ import { DocumentLoader, PortableObjectVerifier } from "@fedify/vocab-runtime";
6
5
  //#region src/sig/proof.d.ts
7
6
  /**
8
7
  * Checks if the given JSON-LD document has a DataIntegrityProof-like object,
@@ -1,8 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Actor, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1 } from "@fedify/vocab";
3
- import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
3
  import { DocumentLoader, PortableObjectVerifier } from "@fedify/vocab-runtime";
5
-
4
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
6
5
  //#region src/sig/proof.d.ts
7
6
  /**
8
7
  * Checks if the given JSON-LD document has a DataIntegrityProof-like object,
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { p as recordDocumentCache } from "./metrics-D3LHk6Z4.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-CpxvUrBz.mjs";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { preloadedContexts } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/kv-cache.ts
@@ -1,9 +1,10 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { ct as recordDocumentCache, q as validateCryptoKey, t as doubleKnock } from "./http-DlNxxW_5.js";
3
+ import { X as validateCryptoKey, dt as recordDocumentCache, t as doubleKnockScoped } from "./http-ChXUU_3c.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
- import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, resolveDocumentLoaderTimeout, validatePublicUrl, withDocumentLoaderTimeout } from "@fedify/vocab-runtime";
5
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, resolveDocumentLoaderTimeout, withDocumentLoaderTimeout } from "@fedify/vocab-runtime";
6
6
  import { curry } from "es-toolkit";
7
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
7
8
  //#region src/utils/docloader.ts
8
9
  const logger$1 = getLogger([
9
10
  "fedify",
@@ -27,13 +28,35 @@ const DEFAULT_MAX_REDIRECTION = 20;
27
28
  * @throws {RangeError} If the `timeout` option is invalid.
28
29
  * @since 0.4.0
29
30
  */
30
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}) {
31
+ function getAuthenticatedDocumentLoader(identity, options = {}) {
32
+ return createAuthenticatedDocumentLoader(identity, options);
33
+ }
34
+ /**
35
+ * The placeholder that the alternate document callback of
36
+ * `getRemoteDocument()` returns, so that the alternate document is loaded
37
+ * after the current hop is closed.
38
+ */
39
+ const ALTERNATE_PLACEHOLDER = Object.freeze({
40
+ contextUrl: null,
41
+ document: null,
42
+ documentUrl: "about:blank"
43
+ });
44
+ /**
45
+ * The implementation of {@link getAuthenticatedDocumentLoader}. This is not
46
+ * exported from any entry point.
47
+ * @param identity The identity to get the document loader for.
48
+ * @param options The options for the document loader.
49
+ * @param deps The dependencies, for tests.
50
+ * @returns The authenticated document loader.
51
+ * @throws {TypeError} If the key is invalid or unsupported.
52
+ */
53
+ function createAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}, deps = {}) {
31
54
  validateCryptoKey(identity.privateKey);
32
55
  const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
33
56
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
34
57
  options?.signal?.throwIfAborted();
35
58
  let currentUrl = new URL(url).href;
36
- await validateUrl(currentUrl, options);
59
+ validateUrl(currentUrl);
37
60
  visited.add(currentUrl);
38
61
  const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
39
62
  function follow(nextUrl) {
@@ -42,46 +65,68 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
42
65
  if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
43
66
  redirected++;
44
67
  }
45
- const response = await doubleKnock(originalRequest, identity, {
68
+ const result = await doubleKnockScoped(originalRequest, identity, {
46
69
  maxRedirection,
47
70
  specDeterminer,
48
71
  log: curry(logRequest)(logger$1),
49
72
  tracerProvider,
50
73
  signal: options?.signal,
51
- validateRedirect: async (nextUrl) => {
74
+ validateRedirect: (nextUrl) => {
52
75
  follow(nextUrl);
53
- await validateUrl(nextUrl, options);
76
+ validateUrl(nextUrl);
54
77
  visited.add(nextUrl);
55
78
  currentUrl = nextUrl;
56
79
  }
80
+ }, (hopUrl) => openHop(hopUrl, options), async (response) => {
81
+ options?.signal?.throwIfAborted();
82
+ let alternateUrl;
83
+ const document = await getRemoteDocument(currentUrl, response, (url) => {
84
+ alternateUrl = url;
85
+ return Promise.resolve(ALTERNATE_PLACEHOLDER);
86
+ }, options);
87
+ if (alternateUrl == null) return { document };
88
+ if (!response.bodyUsed && response.body?.locked !== true) response.body?.cancel().catch((error) => {
89
+ logger$1.debug("Failed to cancel the response body from {url}: {error}", {
90
+ url: currentUrl,
91
+ error
92
+ });
93
+ });
94
+ return { alternateUrl };
57
95
  });
58
96
  options?.signal?.throwIfAborted();
59
- return getRemoteDocument(currentUrl, response, (alternateUrl) => {
60
- follow(alternateUrl);
61
- return load(alternateUrl, options, redirected, visited);
62
- }, options);
97
+ if ("document" in result) return result.document;
98
+ follow(result.alternateUrl);
99
+ return load(result.alternateUrl, options, redirected, visited);
100
+ }
101
+ function openUnvalidatedHop(url, signal) {
102
+ if (deps.openHop != null) return deps.openHop(url);
103
+ return allowPrivateAddress ? openDirectFetch(url) : openPublicFetch(url, { signal });
104
+ }
105
+ async function openHop(url, options) {
106
+ try {
107
+ const session = await openUnvalidatedHop(url, options?.signal);
108
+ if (options?.signal?.aborted) {
109
+ await session.close();
110
+ options.signal.throwIfAborted();
111
+ }
112
+ return session;
113
+ } catch (error) {
114
+ if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
115
+ url,
116
+ error
117
+ });
118
+ else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
119
+ url,
120
+ error
121
+ });
122
+ throw error;
123
+ }
63
124
  }
64
- async function validateUrl(url, options) {
125
+ function validateUrl(url) {
65
126
  const parsedUrl = new URL(url);
66
127
  const protocol = parsedUrl.protocol;
67
128
  if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
68
129
  if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
69
- if (!allowPrivateAddress) {
70
- try {
71
- await validatePublicUrl(url);
72
- } catch (error) {
73
- if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
74
- url,
75
- error
76
- });
77
- else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
78
- url,
79
- error
80
- });
81
- throw error;
82
- }
83
- options?.signal?.throwIfAborted();
84
- }
85
130
  }
86
131
  return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
87
132
  }