@fedify/fedify 2.0.35-dev.2480 → 2.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{builder-cEjyBwpD.mjs → builder-CpxZPa7I.mjs} +3 -3
- package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
- package/dist/{client-BUqbea1d.mjs → client-DGNacEx_.mjs} +113 -64
- package/dist/{client-CSddvgWN.d.ts → client-jE7oGvRh.d.ts} +0 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-r8sjhltN.d.cts → context-CnoWxuoB.d.cts} +2 -2
- package/dist/{context-B2iX3heK.d.ts → context-lxjaUTAI.d.ts} +2 -2
- package/dist/{deno-BcnGeGr2.mjs → deno-BAtjc8bx.mjs} +1 -1
- package/dist/docloader-CyBB50Om.mjs +124 -0
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +6 -6
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +42 -23
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/send.test.mjs +445 -70
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{http-BD9izRp9.cjs → http-5u2wDmjl.cjs} +114 -56
- package/dist/{http-D0cjeL8a.mjs → http-9wCo74JF.mjs} +136 -49
- package/dist/{http-Bs6thuCC.d.cts → http-Cd8UD1ym.d.ts} +1 -2
- package/dist/{http-CDupyfOc.d.ts → http-DFqAb11b.d.cts} +1 -2
- package/dist/{http-IYRDhhnT.js → http-Dwj7bNhv.js} +113 -55
- package/dist/{inbox-D2fsxjqC.mjs → inbox-De2IaCfd.mjs} +1 -1
- package/dist/{key-BFzQ3Hef.mjs → key-BD-PBpNB.mjs} +1 -1
- package/dist/{kv-cache-C-RyNMt6.js → kv-cache-5TdAMHng.js} +60 -18
- package/dist/{kv-cache-B4sDuLjb.cjs → kv-cache-ivTOKx4b.cjs} +59 -17
- package/dist/{ld-D2bkYBIz.mjs → ld-34_mZbyP.mjs} +2 -2
- package/dist/{middleware-3mc-kq3v.mjs → middleware-BI7Hldd0.mjs} +1 -1
- package/dist/{middleware-DxX8ZL-q.cjs → middleware-Co4IpMca.cjs} +118 -33
- package/dist/{middleware-DRZp7Odv.js → middleware-DTJ9dnYC.js} +118 -33
- package/dist/{middleware-B2UC8mtN.cjs → middleware-DhwZ4gNG.cjs} +1 -1
- package/dist/{middleware-CXIBpN4y.mjs → middleware-DzaMP9BP.mjs} +12 -12
- package/dist/{mod-BkPB4hPe.d.cts → mod-BPDfu4Ey.d.cts} +2 -7
- package/dist/{mod-E7YM72JZ.d.ts → mod-BS-jg3Wx.d.ts} +1 -1
- package/dist/{mod-BKJyD7nk.d.ts → mod-DGP89q0u.d.ts} +2 -7
- package/dist/{mod-IOWUF5Yr.d.cts → mod-DVPS4yam.d.cts} +1 -1
- package/dist/{mod-CM_eR8hQ.d.ts → mod-I-34OMN3.d.ts} +1 -1
- package/dist/{mod-DDTLbfce.d.cts → mod-w1oyHseQ.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/client.test.mjs +337 -19
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/{owner-BuJXJDhl.mjs → owner-CxBLBD2K.mjs} +2 -2
- package/dist/{proof-BJ_6nKhb.js → proof-D5IUmaZ0.js} +1 -1
- package/dist/{proof-JxVhLZA3.mjs → proof-DhW1tGUz.mjs} +2 -2
- package/dist/{proof-DfmHAmtr.cjs → proof-yX6qSpxk.cjs} +1 -1
- package/dist/{send-CDaWmeqV.mjs → send-CdfEXNS7.mjs} +117 -32
- package/dist/sig/http.test.mjs +411 -2
- package/dist/sig/key.test.mjs +2 -2
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +1 -1
- package/dist/{temporal-Bv6Zn1C5.mjs → temporal-BbywNJ81.mjs} +1 -1
- package/dist/testing/mod.d.mts +0 -1
- package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
- package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
- package/dist/utils/docloader.test.mjs +316 -32
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +5 -5
- package/dist/docloader-BqDq3fDF.mjs +0 -82
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as version, t as name } from "./deno-
|
|
5
|
-
import {
|
|
4
|
+
import { n as version, t as name } from "./deno-BAtjc8bx.mjs";
|
|
5
|
+
import { r as doubleKnockScoped } from "./http-9wCo74JF.mjs";
|
|
6
6
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
7
|
-
import { FetchError, UrlError
|
|
7
|
+
import { FetchError, UrlError } from "@fedify/vocab-runtime";
|
|
8
|
+
import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
|
|
8
9
|
import { getLogger } from "@logtape/logtape";
|
|
9
10
|
//#region src/federation/send.ts
|
|
10
11
|
/**
|
|
@@ -38,9 +39,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
|
|
|
38
39
|
*
|
|
39
40
|
* @param parameters The parameters for sending the activity.
|
|
40
41
|
* See also {@link SendActivityParameters}.
|
|
42
|
+
* @param deps The dependencies, for tests.
|
|
41
43
|
* @throws {Error} If the activity fails to send.
|
|
42
44
|
*/
|
|
43
|
-
function sendActivity(options) {
|
|
45
|
+
function sendActivity(options, deps = {}) {
|
|
44
46
|
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
45
47
|
return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
|
|
46
48
|
kind: SpanKind.CLIENT,
|
|
@@ -52,7 +54,7 @@ function sendActivity(options) {
|
|
|
52
54
|
await sendActivityInternal({
|
|
53
55
|
...options,
|
|
54
56
|
tracerProvider
|
|
55
|
-
}, span);
|
|
57
|
+
}, span, deps);
|
|
56
58
|
} catch (e) {
|
|
57
59
|
span.setStatus({
|
|
58
60
|
code: SpanStatusCode.ERROR,
|
|
@@ -86,21 +88,25 @@ async function readLimitedResponseBody(response, maxBytes) {
|
|
|
86
88
|
totalBytes += value.length;
|
|
87
89
|
}
|
|
88
90
|
} finally {
|
|
91
|
+
if (truncated) reader.cancel().catch(() => {});
|
|
89
92
|
reader.releaseLock();
|
|
90
93
|
}
|
|
91
94
|
let result = chunks.join("");
|
|
92
95
|
if (truncated) result += "… (truncated)";
|
|
93
96
|
return result;
|
|
94
97
|
}
|
|
95
|
-
async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
|
|
98
|
+
async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span, deps) {
|
|
96
99
|
const logger = getLogger([
|
|
97
100
|
"fedify",
|
|
98
101
|
"federation",
|
|
99
102
|
"outbox"
|
|
100
103
|
]);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? openDirectFetch : (url) => openPublicFetch(url));
|
|
105
|
+
let opened = false;
|
|
106
|
+
async function openHop(url) {
|
|
107
|
+
let session;
|
|
108
|
+
try {
|
|
109
|
+
session = await openUnvalidatedHop(url);
|
|
104
110
|
} catch (error) {
|
|
105
111
|
if (error instanceof UrlError && error.reason === "dns") {
|
|
106
112
|
logger.error("DNS resolution failed for URL: {url}", {
|
|
@@ -117,8 +123,9 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
|
|
|
117
123
|
});
|
|
118
124
|
throw error;
|
|
119
125
|
}
|
|
126
|
+
opened = true;
|
|
127
|
+
return session;
|
|
120
128
|
}
|
|
121
|
-
await validateUrl(inbox.href);
|
|
122
129
|
headers = new Headers(headers);
|
|
123
130
|
headers.set("Content-Type", "application/activity+json");
|
|
124
131
|
const request = new Request(inbox, {
|
|
@@ -139,36 +146,30 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
|
|
|
139
146
|
privateKey: pair.privateKey
|
|
140
147
|
}))
|
|
141
148
|
});
|
|
142
|
-
let
|
|
149
|
+
let outcome;
|
|
143
150
|
try {
|
|
144
|
-
|
|
151
|
+
outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openHop, (response) => consumeSendActivityResponse(response, inbox)) : await doubleKnockScoped(request, rsaKey, {
|
|
145
152
|
tracerProvider,
|
|
146
|
-
specDeterminer
|
|
147
|
-
|
|
148
|
-
});
|
|
153
|
+
specDeterminer
|
|
154
|
+
}, openHop, (response) => consumeSendActivityResponse(response, inbox));
|
|
149
155
|
} catch (error) {
|
|
150
|
-
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
156
|
+
if (opened) logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
151
157
|
activityId,
|
|
152
158
|
inbox: inbox.href,
|
|
153
159
|
error
|
|
154
160
|
});
|
|
155
161
|
throw error;
|
|
156
162
|
}
|
|
157
|
-
if (!
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
161
|
-
} catch (_) {
|
|
162
|
-
error = "";
|
|
163
|
-
}
|
|
163
|
+
if (!outcome.ok) {
|
|
164
|
+
const { status, statusText, error } = outcome;
|
|
164
165
|
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
165
166
|
activityId,
|
|
166
167
|
inbox: inbox.href,
|
|
167
|
-
status
|
|
168
|
-
statusText
|
|
168
|
+
status,
|
|
169
|
+
statusText,
|
|
169
170
|
error
|
|
170
171
|
});
|
|
171
|
-
throw new SendActivityError(inbox,
|
|
172
|
+
throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error);
|
|
172
173
|
}
|
|
173
174
|
span.addEvent("activitypub.activity.sent", {
|
|
174
175
|
"activitypub.activity.json": JSON.stringify(activity),
|
|
@@ -176,10 +177,74 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
|
|
|
176
177
|
"activitypub.activity.id": activityId ?? ""
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Starts cancelling the body of a response without waiting for it to settle.
|
|
182
|
+
* The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
|
|
183
|
+
* settles only after the other branch is cancelled too, per the Streams
|
|
184
|
+
* standard, so awaiting it could block forever. Failures are only logged.
|
|
185
|
+
*/
|
|
186
|
+
function cancelResponseBody(response, url) {
|
|
187
|
+
response.body?.cancel().catch((error) => {
|
|
188
|
+
getLogger([
|
|
189
|
+
"fedify",
|
|
190
|
+
"federation",
|
|
191
|
+
"outbox"
|
|
192
|
+
]).debug("Failed to cancel the response body from {url}: {error}", {
|
|
193
|
+
url,
|
|
194
|
+
error
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Reads what is needed from the final response of a delivery while its
|
|
200
|
+
* session is open: the body of a successful response is cancelled, and at
|
|
201
|
+
* most 1 KiB of an error response body is read.
|
|
202
|
+
*/
|
|
203
|
+
async function consumeSendActivityResponse(response, inbox) {
|
|
204
|
+
if (response.ok) {
|
|
205
|
+
cancelResponseBody(response, response.url || inbox.href);
|
|
206
|
+
return { ok: true };
|
|
207
|
+
}
|
|
208
|
+
let error;
|
|
209
|
+
try {
|
|
210
|
+
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
211
|
+
} catch (_) {
|
|
212
|
+
error = "";
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
status: response.status,
|
|
217
|
+
statusText: response.statusText,
|
|
218
|
+
error
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Sends a request without signing it, following redirects manually like the
|
|
223
|
+
* Fetch standard does. Every hop is opened with `openHop()`, which validates
|
|
224
|
+
* the destination, after the previous hop is closed. The final response is
|
|
225
|
+
* passed to `consume()`, and its session is closed after that.
|
|
226
|
+
* This is exported only for tests; it is not exported from any entry point.
|
|
227
|
+
* @param request The request to send.
|
|
228
|
+
* @param openHop The function to open a session for each hop.
|
|
229
|
+
* @param consume The function to consume the final response.
|
|
230
|
+
* @returns The value returned by `consume`.
|
|
231
|
+
*/
|
|
232
|
+
async function fetchWithValidatedRedirects(request, openHop, consume) {
|
|
233
|
+
const logger = getLogger([
|
|
234
|
+
"fedify",
|
|
235
|
+
"federation",
|
|
236
|
+
"outbox"
|
|
237
|
+
]);
|
|
180
238
|
let body = await request.clone().text();
|
|
181
239
|
for (let redirects = 0;; redirects++) {
|
|
182
|
-
const
|
|
240
|
+
const session = await openHop(request.url);
|
|
241
|
+
let response;
|
|
242
|
+
try {
|
|
243
|
+
response = await session.fetch(request, { redirect: "manual" });
|
|
244
|
+
} catch (error) {
|
|
245
|
+
await closeAfterError(session, logger);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
183
248
|
const location = response.headers.get("Location");
|
|
184
249
|
if (![
|
|
185
250
|
301,
|
|
@@ -187,11 +252,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
|
187
252
|
303,
|
|
188
253
|
307,
|
|
189
254
|
308
|
|
190
|
-
].includes(response.status) || location == null)
|
|
191
|
-
|
|
255
|
+
].includes(response.status) || location == null) try {
|
|
256
|
+
return await consume(response);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
|
|
259
|
+
url: request.url,
|
|
260
|
+
error
|
|
261
|
+
});
|
|
262
|
+
throw error;
|
|
263
|
+
} finally {
|
|
264
|
+
await session.close();
|
|
265
|
+
}
|
|
266
|
+
cancelResponseBody(response, request.url);
|
|
267
|
+
await session.close();
|
|
192
268
|
if (redirects >= 20) throw new TypeError("Too many redirects");
|
|
193
269
|
const url = new URL(location, request.url);
|
|
194
|
-
await validateUrl(url.href);
|
|
195
270
|
const headers = new Headers(request.headers);
|
|
196
271
|
if (url.origin !== new URL(request.url).origin) {
|
|
197
272
|
headers.delete("Authorization");
|
|
@@ -219,6 +294,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
|
219
294
|
});
|
|
220
295
|
}
|
|
221
296
|
}
|
|
297
|
+
async function closeAfterError(session, logger) {
|
|
298
|
+
try {
|
|
299
|
+
await session.close();
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logger.debug("Failed to close the session for {url}: {error}", {
|
|
302
|
+
url: session.url.href,
|
|
303
|
+
error
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
222
307
|
/**
|
|
223
308
|
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
224
309
|
* It contains structured information about the failure, including the HTTP
|
|
@@ -257,4 +342,4 @@ var SendActivityError = class extends Error {
|
|
|
257
342
|
}
|
|
258
343
|
};
|
|
259
344
|
//#endregion
|
|
260
|
-
export { extractInboxes as n,
|
|
345
|
+
export { sendActivity as i, extractInboxes as n, fetchWithValidatedRedirects as r, SendActivityError as t };
|
package/dist/sig/http.test.mjs
CHANGED
|
@@ -7,11 +7,13 @@ import { i as assertExists, t as assertStringIncludes } from "../std__assert-Dfl
|
|
|
7
7
|
import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
|
|
8
8
|
import { t as assert } from "../assert-BhQd8QHP.mjs";
|
|
9
9
|
import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
|
|
10
|
-
import { t as exportJwk } from "../key-
|
|
11
|
-
import { a as
|
|
10
|
+
import { t as exportJwk } from "../key-BD-PBpNB.mjs";
|
|
11
|
+
import { a as formatRfc9421SignatureParameters, c as signRequest, i as formatRfc9421Signature, l as timingSafeEqual, n as doubleKnock, o as parseRfc9421Signature, r as doubleKnockScoped, s as parseRfc9421SignatureInput, t as createRfc9421SignatureBase, u as verifyRequest } from "../http-9wCo74JF.mjs";
|
|
12
12
|
import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-CCxRJhWa.mjs";
|
|
13
13
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
14
14
|
import { FetchError, exportSpki } from "@fedify/vocab-runtime";
|
|
15
|
+
import { openDirectFetch } from "@fedify/vocab-runtime/internal";
|
|
16
|
+
import { configure, reset } from "@logtape/logtape";
|
|
15
17
|
import { encodeBase64 } from "byte-encodings/base64";
|
|
16
18
|
//#region src/sig/http.test.ts
|
|
17
19
|
test("signRequest() [draft-cavage]", async () => {
|
|
@@ -1054,6 +1056,330 @@ test("doubleKnock() async specDeterminer test", async () => {
|
|
|
1054
1056
|
assertEquals(specUsed, "draft-cavage-http-signatures-12", "Should use spec from async determiner");
|
|
1055
1057
|
esm_default.hardReset();
|
|
1056
1058
|
});
|
|
1059
|
+
const lifecycleIdentity = {
|
|
1060
|
+
keyId: rsaPublicKey2.id,
|
|
1061
|
+
privateKey: rsaPrivateKey2
|
|
1062
|
+
};
|
|
1063
|
+
test("doubleKnockScoped() retries another spec on the same hop", async () => {
|
|
1064
|
+
const url = "https://example.com/lifecycle/retry";
|
|
1065
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1066
|
+
status: 401,
|
|
1067
|
+
body: "Unauthorized"
|
|
1068
|
+
} : {
|
|
1069
|
+
status: 202,
|
|
1070
|
+
body: "Accepted"
|
|
1071
|
+
});
|
|
1072
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
body: "activity"
|
|
1075
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), "Accepted");
|
|
1076
|
+
assertEquals(hops.events, [
|
|
1077
|
+
`open ${url}`,
|
|
1078
|
+
`fetch #1 ${url}`,
|
|
1079
|
+
"cancel #1",
|
|
1080
|
+
`fetch #2 ${url}`,
|
|
1081
|
+
"consumed #2",
|
|
1082
|
+
`close ${url}`
|
|
1083
|
+
]);
|
|
1084
|
+
assertEquals(hops.requests.map((r) => r.headers.has("Signature-Input")), [true, false]);
|
|
1085
|
+
hops.assertInvariants();
|
|
1086
|
+
});
|
|
1087
|
+
test("doubleKnockScoped() closes the retried hop before a redirect", async () => {
|
|
1088
|
+
const url = "https://example.com/lifecycle/retry-redirect";
|
|
1089
|
+
const next = "https://example.com/lifecycle/final";
|
|
1090
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1091
|
+
status: 401,
|
|
1092
|
+
body: "Unauthorized"
|
|
1093
|
+
} : index === 2 ? {
|
|
1094
|
+
status: 302,
|
|
1095
|
+
headers: { Location: next },
|
|
1096
|
+
body: "Found"
|
|
1097
|
+
} : {
|
|
1098
|
+
status: 200,
|
|
1099
|
+
body: "Done"
|
|
1100
|
+
});
|
|
1101
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1102
|
+
method: "POST",
|
|
1103
|
+
body: "activity"
|
|
1104
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), "Done");
|
|
1105
|
+
assertEquals(hops.events, [
|
|
1106
|
+
`open ${url}`,
|
|
1107
|
+
`fetch #1 ${url}`,
|
|
1108
|
+
"cancel #1",
|
|
1109
|
+
`fetch #2 ${url}`,
|
|
1110
|
+
"cancel #2",
|
|
1111
|
+
`close ${url}`,
|
|
1112
|
+
`open ${next}`,
|
|
1113
|
+
`fetch #3 ${next}`,
|
|
1114
|
+
"consumed #3",
|
|
1115
|
+
`close ${next}`
|
|
1116
|
+
]);
|
|
1117
|
+
assertEquals(await hops.requests[2].text(), "activity");
|
|
1118
|
+
hops.assertInvariants();
|
|
1119
|
+
});
|
|
1120
|
+
test("doubleKnockScoped() retries transport errors on the same hop", async (t) => {
|
|
1121
|
+
await t.step("GET", async () => {
|
|
1122
|
+
const url = "https://example.com/lifecycle/flaky";
|
|
1123
|
+
const hops = recordHops((_, index) => {
|
|
1124
|
+
if (index === 1) throw new TypeError("connection reset");
|
|
1125
|
+
return { body: "Success" };
|
|
1126
|
+
});
|
|
1127
|
+
assertEquals(await doubleKnockScoped(new Request(url), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), "Success");
|
|
1128
|
+
assertEquals(hops.events, [
|
|
1129
|
+
`open ${url}`,
|
|
1130
|
+
`fetch #1 ${url}`,
|
|
1131
|
+
`fetch #2 ${url}`,
|
|
1132
|
+
"consumed #2",
|
|
1133
|
+
`close ${url}`
|
|
1134
|
+
]);
|
|
1135
|
+
hops.assertInvariants();
|
|
1136
|
+
});
|
|
1137
|
+
await t.step("POST", async () => {
|
|
1138
|
+
const url = "https://example.com/lifecycle/flaky-inbox";
|
|
1139
|
+
const hops = recordHops(() => {
|
|
1140
|
+
throw new TypeError("connection reset");
|
|
1141
|
+
});
|
|
1142
|
+
await assertRejects(() => doubleKnockScoped(new Request(url, {
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
body: "activity"
|
|
1145
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), FetchError);
|
|
1146
|
+
assertEquals(hops.events, [
|
|
1147
|
+
`open ${url}`,
|
|
1148
|
+
`fetch #1 ${url}`,
|
|
1149
|
+
`close ${url}`
|
|
1150
|
+
]);
|
|
1151
|
+
hops.assertInvariants();
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
async function captureLogs(fn) {
|
|
1155
|
+
const records = [];
|
|
1156
|
+
await configure({
|
|
1157
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1158
|
+
loggers: [{
|
|
1159
|
+
category: "fedify",
|
|
1160
|
+
sinks: ["buffer"],
|
|
1161
|
+
lowestLevel: "debug"
|
|
1162
|
+
}, {
|
|
1163
|
+
category: ["logtape", "meta"],
|
|
1164
|
+
sinks: []
|
|
1165
|
+
}],
|
|
1166
|
+
reset: true
|
|
1167
|
+
});
|
|
1168
|
+
try {
|
|
1169
|
+
await fn();
|
|
1170
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1171
|
+
} finally {
|
|
1172
|
+
await reset();
|
|
1173
|
+
}
|
|
1174
|
+
return records;
|
|
1175
|
+
}
|
|
1176
|
+
test("doubleKnockScoped() logs a redirect body failing to cancel and follows the redirect", async () => {
|
|
1177
|
+
const url = "https://example.com/lifecycle/cancel-failure";
|
|
1178
|
+
const next = "https://example.com/lifecycle/cancel-failure-final";
|
|
1179
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1180
|
+
status: 302,
|
|
1181
|
+
headers: { Location: next },
|
|
1182
|
+
body: "Found",
|
|
1183
|
+
failCancel: true
|
|
1184
|
+
} : {
|
|
1185
|
+
status: 200,
|
|
1186
|
+
body: "Done"
|
|
1187
|
+
});
|
|
1188
|
+
let result;
|
|
1189
|
+
const records = await captureLogs(async () => {
|
|
1190
|
+
result = await doubleKnockScoped(new Request(url), lifecycleIdentity, {}, hops.openHop, (response) => response.text());
|
|
1191
|
+
});
|
|
1192
|
+
assertEquals(result, "Done");
|
|
1193
|
+
assertEquals(hops.events, [
|
|
1194
|
+
`open ${url}`,
|
|
1195
|
+
`fetch #1 ${url}`,
|
|
1196
|
+
"cancel #1",
|
|
1197
|
+
`close ${url}`,
|
|
1198
|
+
`open ${next}`,
|
|
1199
|
+
`fetch #2 ${next}`,
|
|
1200
|
+
"consumed #2",
|
|
1201
|
+
`close ${next}`
|
|
1202
|
+
]);
|
|
1203
|
+
const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
|
|
1204
|
+
assertEquals(failures.map((r) => [r.level, r.properties.url]), [["debug", url]]);
|
|
1205
|
+
assertEquals(failures[0].properties.error.message, "cancel #1 failed");
|
|
1206
|
+
hops.assertInvariants();
|
|
1207
|
+
});
|
|
1208
|
+
test("doubleKnockScoped() is not blocked by a body cancellation that never settles", async () => {
|
|
1209
|
+
const url = "https://example.com/lifecycle/hanging-cancel";
|
|
1210
|
+
const next = "https://example.com/lifecycle/hanging-cancel-final";
|
|
1211
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1212
|
+
status: 401,
|
|
1213
|
+
body: "Unauthorized",
|
|
1214
|
+
hangCancel: true
|
|
1215
|
+
} : index === 2 ? {
|
|
1216
|
+
status: 302,
|
|
1217
|
+
headers: { Location: next },
|
|
1218
|
+
body: "Found",
|
|
1219
|
+
hangCancel: true
|
|
1220
|
+
} : {
|
|
1221
|
+
status: 200,
|
|
1222
|
+
body: "Done",
|
|
1223
|
+
hangCancel: true
|
|
1224
|
+
});
|
|
1225
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1226
|
+
method: "POST",
|
|
1227
|
+
body: "activity"
|
|
1228
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => Promise.resolve(response.status)), 200);
|
|
1229
|
+
assertEquals(hops.events, [
|
|
1230
|
+
`open ${url}`,
|
|
1231
|
+
`fetch #1 ${url}`,
|
|
1232
|
+
"cancel #1",
|
|
1233
|
+
`fetch #2 ${url}`,
|
|
1234
|
+
"cancel #2",
|
|
1235
|
+
`close ${url}`,
|
|
1236
|
+
`open ${next}`,
|
|
1237
|
+
`fetch #3 ${next}`,
|
|
1238
|
+
"cancel #3 by close",
|
|
1239
|
+
`close ${next}`
|
|
1240
|
+
]);
|
|
1241
|
+
hops.assertInvariants();
|
|
1242
|
+
});
|
|
1243
|
+
test("doubleKnockScoped() completes with responses cloned by fetch-mock", async () => {
|
|
1244
|
+
esm_default.spyGlobal();
|
|
1245
|
+
try {
|
|
1246
|
+
let attempts = 0;
|
|
1247
|
+
esm_default.post("https://example.com/tee/redirect", () => {
|
|
1248
|
+
attempts++;
|
|
1249
|
+
return attempts === 1 ? new Response("Unauthorized", { status: 401 }) : new Response("Found", {
|
|
1250
|
+
status: 302,
|
|
1251
|
+
headers: { Location: "https://example.com/tee/final" }
|
|
1252
|
+
});
|
|
1253
|
+
});
|
|
1254
|
+
esm_default.post("https://example.com/tee/final", () => new Response("Accepted", { status: 202 }));
|
|
1255
|
+
const request = () => new Request("https://example.com/tee/redirect", {
|
|
1256
|
+
method: "POST",
|
|
1257
|
+
body: "activity"
|
|
1258
|
+
});
|
|
1259
|
+
assertEquals(await doubleKnockScoped(request(), lifecycleIdentity, {}, openDirectFetch, (response) => Promise.resolve(response.status)), 202);
|
|
1260
|
+
assertEquals(attempts, 2);
|
|
1261
|
+
attempts = 0;
|
|
1262
|
+
const response = await doubleKnock(request(), lifecycleIdentity);
|
|
1263
|
+
assertEquals(response.status, 202);
|
|
1264
|
+
assertEquals(await response.text(), "Accepted");
|
|
1265
|
+
assertEquals(attempts, 2);
|
|
1266
|
+
} finally {
|
|
1267
|
+
esm_default.hardReset();
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
test("doubleKnockScoped() closes the session when consuming fails", async (t) => {
|
|
1271
|
+
const url = "https://example.com/lifecycle/consume";
|
|
1272
|
+
await t.step("exception", async () => {
|
|
1273
|
+
const hops = recordHops(() => ({ body: "x".repeat(1024) }));
|
|
1274
|
+
const error = /* @__PURE__ */ new Error("consume failed");
|
|
1275
|
+
await assertRejects(() => doubleKnockScoped(new Request(url), lifecycleIdentity, {}, hops.openHop, async (response) => {
|
|
1276
|
+
const reader = response.body.getReader();
|
|
1277
|
+
await reader.read();
|
|
1278
|
+
reader.releaseLock();
|
|
1279
|
+
throw error;
|
|
1280
|
+
}), Error, "consume failed");
|
|
1281
|
+
assertEquals(hops.events, [
|
|
1282
|
+
`open ${url}`,
|
|
1283
|
+
`fetch #1 ${url}`,
|
|
1284
|
+
"cancel #1 by close",
|
|
1285
|
+
`close ${url}`
|
|
1286
|
+
]);
|
|
1287
|
+
hops.assertInvariants();
|
|
1288
|
+
});
|
|
1289
|
+
await t.step("abort", async () => {
|
|
1290
|
+
const hops = recordHops(() => ({ body: "Success" }));
|
|
1291
|
+
const controller = new AbortController();
|
|
1292
|
+
const reason = /* @__PURE__ */ new Error("aborted while consuming");
|
|
1293
|
+
assert(await assertRejects(() => doubleKnockScoped(new Request(url), lifecycleIdentity, { signal: controller.signal }, hops.openHop, () => {
|
|
1294
|
+
controller.abort(reason);
|
|
1295
|
+
controller.signal.throwIfAborted();
|
|
1296
|
+
return Promise.resolve();
|
|
1297
|
+
})) === reason);
|
|
1298
|
+
assertEquals(hops.events, [
|
|
1299
|
+
`open ${url}`,
|
|
1300
|
+
`fetch #1 ${url}`,
|
|
1301
|
+
"cancel #1 by close",
|
|
1302
|
+
`close ${url}`
|
|
1303
|
+
]);
|
|
1304
|
+
hops.assertInvariants();
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
test("doubleKnock() returns the final response without cancelling its body", async () => {
|
|
1308
|
+
const url = "https://example.com/lifecycle/public";
|
|
1309
|
+
const next = "https://example.com/lifecycle/public-final";
|
|
1310
|
+
const events = [];
|
|
1311
|
+
const responses = [];
|
|
1312
|
+
const originalFetch = globalThis.fetch;
|
|
1313
|
+
globalThis.fetch = ((input) => {
|
|
1314
|
+
const requestUrl = input instanceof Request ? input.url : String(input);
|
|
1315
|
+
const index = responses.length + 1;
|
|
1316
|
+
events.push(`fetch #${index} ${requestUrl}`);
|
|
1317
|
+
const recorded = createRecordedResponse(events, index, requestUrl === url ? {
|
|
1318
|
+
status: 302,
|
|
1319
|
+
headers: { Location: next },
|
|
1320
|
+
body: "Found"
|
|
1321
|
+
} : {
|
|
1322
|
+
status: 200,
|
|
1323
|
+
body: "Done"
|
|
1324
|
+
});
|
|
1325
|
+
responses.push(recorded);
|
|
1326
|
+
return Promise.resolve(recorded.response);
|
|
1327
|
+
});
|
|
1328
|
+
try {
|
|
1329
|
+
const response = await doubleKnock(new Request(url), lifecycleIdentity);
|
|
1330
|
+
assertEquals(events, [
|
|
1331
|
+
`fetch #1 ${url}`,
|
|
1332
|
+
"cancel #1",
|
|
1333
|
+
`fetch #2 ${next}`
|
|
1334
|
+
]);
|
|
1335
|
+
assertFalse(responses[1].settled());
|
|
1336
|
+
assertEquals(await response.text(), "Done");
|
|
1337
|
+
assertEquals(events.at(-1), "consumed #2");
|
|
1338
|
+
} finally {
|
|
1339
|
+
globalThis.fetch = originalFetch;
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
test("doubleKnock() is not blocked by a body cancellation that never settles", async () => {
|
|
1343
|
+
const url = "https://example.com/lifecycle/public-hanging";
|
|
1344
|
+
const next = "https://example.com/lifecycle/public-hanging-final";
|
|
1345
|
+
const events = [];
|
|
1346
|
+
const originalFetch = globalThis.fetch;
|
|
1347
|
+
let index = 0;
|
|
1348
|
+
globalThis.fetch = ((input) => {
|
|
1349
|
+
const requestUrl = input instanceof Request ? input.url : String(input);
|
|
1350
|
+
index++;
|
|
1351
|
+
events.push(`fetch #${index} ${requestUrl}`);
|
|
1352
|
+
return Promise.resolve(createRecordedResponse(events, index, index === 1 ? {
|
|
1353
|
+
status: 401,
|
|
1354
|
+
body: "Unauthorized",
|
|
1355
|
+
hangCancel: true
|
|
1356
|
+
} : index === 2 ? {
|
|
1357
|
+
status: 302,
|
|
1358
|
+
headers: { Location: next },
|
|
1359
|
+
body: "Found",
|
|
1360
|
+
hangCancel: true
|
|
1361
|
+
} : {
|
|
1362
|
+
status: 200,
|
|
1363
|
+
body: "Done"
|
|
1364
|
+
}).response);
|
|
1365
|
+
});
|
|
1366
|
+
try {
|
|
1367
|
+
const response = await doubleKnock(new Request(url, {
|
|
1368
|
+
method: "POST",
|
|
1369
|
+
body: "activity"
|
|
1370
|
+
}), lifecycleIdentity);
|
|
1371
|
+
assertEquals(events, [
|
|
1372
|
+
`fetch #1 ${url}`,
|
|
1373
|
+
"cancel #1",
|
|
1374
|
+
`fetch #2 ${url}`,
|
|
1375
|
+
"cancel #2",
|
|
1376
|
+
`fetch #3 ${next}`
|
|
1377
|
+
]);
|
|
1378
|
+
assertEquals(await response.text(), "Done");
|
|
1379
|
+
} finally {
|
|
1380
|
+
globalThis.fetch = originalFetch;
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1057
1383
|
test("timingSafeEqual()", async (t) => {
|
|
1058
1384
|
await t.step("should return true for equal empty arrays", () => {
|
|
1059
1385
|
assert(timingSafeEqual(new Uint8Array([]), new Uint8Array([])));
|
|
@@ -1382,5 +1708,88 @@ test("signRequest() and verifyRequest() cancellation", {
|
|
|
1382
1708
|
});
|
|
1383
1709
|
esm_default.hardReset();
|
|
1384
1710
|
});
|
|
1711
|
+
function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
|
|
1712
|
+
const bytes = new TextEncoder().encode(init.body ?? "");
|
|
1713
|
+
const chunkSize = init.chunkSize ?? 256;
|
|
1714
|
+
let offset = 0;
|
|
1715
|
+
let settled = false;
|
|
1716
|
+
const body = new ReadableStream({
|
|
1717
|
+
pull(controller) {
|
|
1718
|
+
if (offset >= bytes.length) {
|
|
1719
|
+
settled = true;
|
|
1720
|
+
events.push(`consumed #${index}`);
|
|
1721
|
+
controller.close();
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
controller.enqueue(bytes.slice(offset, offset + chunkSize));
|
|
1725
|
+
offset += chunkSize;
|
|
1726
|
+
},
|
|
1727
|
+
cancel() {
|
|
1728
|
+
settled = true;
|
|
1729
|
+
events.push(`cancel #${index}${cancelSuffix()}`);
|
|
1730
|
+
if (init.failCancel) throw new Error(`cancel #${index} failed`);
|
|
1731
|
+
if (init.hangCancel) return new Promise(() => {});
|
|
1732
|
+
}
|
|
1733
|
+
}, { highWaterMark: 0 });
|
|
1734
|
+
return {
|
|
1735
|
+
response: new Response(body, {
|
|
1736
|
+
status: init.status ?? 200,
|
|
1737
|
+
headers: init.headers
|
|
1738
|
+
}),
|
|
1739
|
+
settled: () => settled
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function recordHops(respond) {
|
|
1743
|
+
const events = [];
|
|
1744
|
+
const requests = [];
|
|
1745
|
+
const unsettledAtClose = [];
|
|
1746
|
+
let opens = 0;
|
|
1747
|
+
let closes = 0;
|
|
1748
|
+
let fetches = 0;
|
|
1749
|
+
return {
|
|
1750
|
+
events,
|
|
1751
|
+
requests,
|
|
1752
|
+
openHop(url) {
|
|
1753
|
+
opens++;
|
|
1754
|
+
events.push(`open ${url}`);
|
|
1755
|
+
const tracked = [];
|
|
1756
|
+
let closed = false;
|
|
1757
|
+
let closing = false;
|
|
1758
|
+
return Promise.resolve({
|
|
1759
|
+
url: new URL(url),
|
|
1760
|
+
async fetch(input, init) {
|
|
1761
|
+
if (closed) throw new TypeError("The session is closed.");
|
|
1762
|
+
const request = new Request(input instanceof Request ? input.clone() : input, init);
|
|
1763
|
+
const index = ++fetches;
|
|
1764
|
+
requests.push(request);
|
|
1765
|
+
events.push(`fetch #${index} ${request.url}`);
|
|
1766
|
+
const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
|
|
1767
|
+
tracked.push({
|
|
1768
|
+
...recorded,
|
|
1769
|
+
index
|
|
1770
|
+
});
|
|
1771
|
+
return recorded.response;
|
|
1772
|
+
},
|
|
1773
|
+
close() {
|
|
1774
|
+
if (closed) return Promise.resolve();
|
|
1775
|
+
closed = true;
|
|
1776
|
+
closes++;
|
|
1777
|
+
closing = true;
|
|
1778
|
+
for (const { response, settled, index } of tracked) {
|
|
1779
|
+
if (settled()) continue;
|
|
1780
|
+
if (response.body?.locked) unsettledAtClose.push(index);
|
|
1781
|
+
else response.body?.cancel().catch(() => {});
|
|
1782
|
+
}
|
|
1783
|
+
events.push(`close ${url}`);
|
|
1784
|
+
return Promise.resolve();
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
},
|
|
1788
|
+
assertInvariants() {
|
|
1789
|
+
assertEquals(opens, closes);
|
|
1790
|
+
assertEquals(unsettledAtClose, []);
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1385
1794
|
//#endregion
|
|
1386
1795
|
export {};
|
package/dist/sig/key.test.mjs
CHANGED
|
@@ -6,9 +6,9 @@ import { n as assertStrictEquals } from "../std__assert-DflMZha5.mjs";
|
|
|
6
6
|
import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
|
|
7
7
|
import { t as assert } from "../assert-BhQd8QHP.mjs";
|
|
8
8
|
import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
|
|
9
|
-
import { a as importJwk, i as generateCryptoKeyPair, o as validateCryptoKey, r as fetchKey, t as exportJwk } from "../key-
|
|
9
|
+
import { a as importJwk, i as generateCryptoKeyPair, o as validateCryptoKey, r as fetchKey, t as exportJwk } from "../key-BD-PBpNB.mjs";
|
|
10
10
|
import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CCxRJhWa.mjs";
|
|
11
|
-
import { g as wrapContextLoaderForJsonLd } from "../ld-
|
|
11
|
+
import { g as wrapContextLoaderForJsonLd } from "../ld-34_mZbyP.mjs";
|
|
12
12
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
13
13
|
import { CryptographicKey, Multikey, Person } from "@fedify/vocab";
|
|
14
14
|
import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
|