@fedify/fedify 2.2.18 → 2.2.20
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-DAbQyxmb.mjs → builder-BolQ56ab.mjs} +2 -2
- package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
- package/dist/{client-9F_u23dc.mjs → client-DhxaVgOv.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-tJbL_Hkw.d.cts → context-CFQGaq5x.d.cts} +28 -2
- package/dist/{context-OkwEpc3-.d.ts → context-DdbLVV_k.d.ts} +28 -2
- package/dist/{deno-JUTr6LYC.mjs → deno-BUj4roJc.mjs} +1 -1
- package/dist/docloader-CbtF_0cD.mjs +125 -0
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/keycache.test.mjs +18 -7
- package/dist/federation/middleware.test.mjs +389 -28
- 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-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
- package/dist/{http-OSYYhKM8.js → http-C5g_VrA_.js} +188 -105
- package/dist/{http-lCmKCZBI.mjs → http-CQMVisM0.mjs} +195 -97
- package/dist/{http-DEH_eppX.cjs → http-D8I7LBfx.cjs} +189 -106
- package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
- package/dist/{inbox-DMATfdEI.mjs → inbox-CD8IDILN.mjs} +1 -1
- package/dist/{key-D0ahOVjf.mjs → key-C3o3DGap.mjs} +17 -3
- package/dist/{keycache-Dkc_sflV.mjs → keycache-BimEQucx.mjs} +1 -1
- package/dist/{kv-cache-B7X4p1y7.js → kv-cache-CSKXXleR.js} +60 -16
- package/dist/{kv-cache-CPEV_4nZ.cjs → kv-cache-DC9LcfoC.cjs} +59 -15
- package/dist/{ld-CJOXIehz.mjs → ld-ijSr-0AB.mjs} +2 -2
- package/dist/{middleware-CHoUjmJm.mjs → middleware-CYAaUTi5.mjs} +79 -39
- package/dist/{middleware-QcuVRA-q.cjs → middleware-DNueR0GD.cjs} +185 -60
- package/dist/{middleware-D3hx1aiI.cjs → middleware-DmcY2TWS.cjs} +1 -1
- package/dist/{middleware-aNUajYPY.js → middleware-sjhz30sm.js} +185 -60
- package/dist/{middleware-BgGJEcdp.mjs → middleware-yqqfI7oJ.mjs} +1 -1
- package/dist/{mod-B6LmugJ4.d.ts → mod-BRnwh2oy.d.ts} +2 -8
- package/dist/{mod-D2JyWtW3.d.cts → mod-BYWp77-z.d.cts} +1 -1
- package/dist/{mod-DQGjRJUG.d.ts → mod-BuRRdsfD.d.ts} +1 -1
- package/dist/{mod-ej_rLx8h.d.ts → mod-CM5_J_cx.d.ts} +1 -1
- package/dist/{mod-a6kmB8xD.d.cts → mod-CVSKw-5H.d.cts} +1 -1
- package/dist/{mod-V2vDnYNu.d.cts → mod-Co7LEK9X.d.cts} +2 -8
- 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/otel/exporter.test.mjs +292 -30
- package/dist/otel/mod.cjs +36 -30
- package/dist/otel/mod.d.cts +13 -0
- package/dist/otel/mod.d.ts +13 -0
- package/dist/otel/mod.js +36 -30
- package/dist/{owner-BUTMEkbn.mjs → owner-B8kXzD0q.mjs} +2 -2
- package/dist/{proof-DS1Zl6BX.mjs → proof-CBQOjOgA.mjs} +2 -2
- package/dist/{proof-Bm3xyn3D.js → proof-DKX6nASL.js} +1 -1
- package/dist/{proof-BeY7TsiS.cjs → proof-qiuGJCD7.cjs} +1 -1
- package/dist/{send-fnPrh9Zm.mjs → send-DRpisX_f.mjs} +117 -32
- package/dist/sig/http.test.mjs +529 -3
- package/dist/sig/key.test.mjs +197 -3
- package/dist/sig/ld.test.mjs +47 -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 +46 -1
- package/dist/sig/proof.test.mjs +52 -1
- package/dist/{temporal-Biz6zK6e.mjs → temporal-BQGEINad.mjs} +1 -1
- package/dist/testing/mod.d.mts +26 -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 +382 -34
- 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 +6 -6
- package/dist/docloader-BGjq1F5U.mjs +0 -81
package/dist/sig/http.test.mjs
CHANGED
|
@@ -7,13 +7,15 @@ import { r as assertExists, t as assertStringIncludes } from "../std__assert-DNZ
|
|
|
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-C3o3DGap.mjs";
|
|
11
|
+
import { a as formatRfc9421SignatureParameters, c as signRequest, d as verifyRequestDetailed, 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-CQMVisM0.mjs";
|
|
12
12
|
import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
|
|
13
|
-
import { t as KvKeyCache } from "../keycache-
|
|
13
|
+
import { t as KvKeyCache } from "../keycache-BimEQucx.mjs";
|
|
14
14
|
import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
|
|
15
15
|
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
16
16
|
import { FetchError, UrlError, exportSpki } from "@fedify/vocab-runtime";
|
|
17
|
+
import { openDirectFetch } from "@fedify/vocab-runtime/internal";
|
|
18
|
+
import { configure, reset } from "@logtape/logtape";
|
|
17
19
|
import { encodeBase64 } from "byte-encodings/base64";
|
|
18
20
|
//#region src/sig/http.test.ts
|
|
19
21
|
test("signRequest() [draft-cavage]", async () => {
|
|
@@ -1259,6 +1261,447 @@ test("doubleKnock() async specDeterminer test", async () => {
|
|
|
1259
1261
|
assertEquals(specUsed, "draft-cavage-http-signatures-12", "Should use spec from async determiner");
|
|
1260
1262
|
esm_default.hardReset();
|
|
1261
1263
|
});
|
|
1264
|
+
const lifecycleIdentity = {
|
|
1265
|
+
keyId: rsaPublicKey2.id,
|
|
1266
|
+
privateKey: rsaPrivateKey2
|
|
1267
|
+
};
|
|
1268
|
+
test("doubleKnockScoped() keeps challenge retries and fallback in the validated hop", async () => {
|
|
1269
|
+
const url = "https://example.com/lifecycle/challenge-fallback";
|
|
1270
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1271
|
+
status: 401,
|
|
1272
|
+
headers: { "Accept-Signature": "challenge=(\"@method\");nonce=\"challenge-nonce\"" },
|
|
1273
|
+
body: "Challenge"
|
|
1274
|
+
} : index === 2 ? {
|
|
1275
|
+
status: 401,
|
|
1276
|
+
body: "Unauthorized"
|
|
1277
|
+
} : {
|
|
1278
|
+
status: 202,
|
|
1279
|
+
body: "Accepted"
|
|
1280
|
+
});
|
|
1281
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1282
|
+
method: "POST",
|
|
1283
|
+
body: "activity"
|
|
1284
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), "Accepted");
|
|
1285
|
+
assertEquals(hops.events, [
|
|
1286
|
+
`open ${url}`,
|
|
1287
|
+
`fetch #1 ${url}`,
|
|
1288
|
+
"cancel #1",
|
|
1289
|
+
`fetch #2 ${url}`,
|
|
1290
|
+
"cancel #2",
|
|
1291
|
+
`fetch #3 ${url}`,
|
|
1292
|
+
"consumed #3",
|
|
1293
|
+
`close ${url}`
|
|
1294
|
+
]);
|
|
1295
|
+
assertStringIncludes(hops.requests[1].headers.get("Signature-Input"), "nonce=\"challenge-nonce\"");
|
|
1296
|
+
assertFalse(hops.requests[2].headers.has("Signature-Input"));
|
|
1297
|
+
hops.assertInvariants();
|
|
1298
|
+
});
|
|
1299
|
+
test("doubleKnockScoped() reopens the session after challenge redirects", async () => {
|
|
1300
|
+
const url = "https://example.com/lifecycle/challenge-redirect";
|
|
1301
|
+
const next = "https://other.example/lifecycle/final";
|
|
1302
|
+
const validated = [];
|
|
1303
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1304
|
+
status: 401,
|
|
1305
|
+
headers: { "Accept-Signature": "challenge=(\"@method\");created" },
|
|
1306
|
+
body: "Challenge"
|
|
1307
|
+
} : index === 2 ? {
|
|
1308
|
+
status: 307,
|
|
1309
|
+
headers: { Location: next },
|
|
1310
|
+
body: "Redirect"
|
|
1311
|
+
} : {
|
|
1312
|
+
status: 200,
|
|
1313
|
+
body: "Done"
|
|
1314
|
+
});
|
|
1315
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1316
|
+
method: "POST",
|
|
1317
|
+
body: "activity"
|
|
1318
|
+
}), lifecycleIdentity, { validateRedirect: (target) => {
|
|
1319
|
+
validated.push(target);
|
|
1320
|
+
} }, hops.openHop, (response) => response.text()), "Done");
|
|
1321
|
+
assertEquals(validated, [next]);
|
|
1322
|
+
assertEquals(hops.events, [
|
|
1323
|
+
`open ${url}`,
|
|
1324
|
+
`fetch #1 ${url}`,
|
|
1325
|
+
"cancel #1",
|
|
1326
|
+
`fetch #2 ${url}`,
|
|
1327
|
+
"cancel #2",
|
|
1328
|
+
`close ${url}`,
|
|
1329
|
+
`open ${next}`,
|
|
1330
|
+
`fetch #3 ${next}`,
|
|
1331
|
+
"consumed #3",
|
|
1332
|
+
`close ${next}`
|
|
1333
|
+
]);
|
|
1334
|
+
assertEquals(await hops.requests[2].text(), "activity");
|
|
1335
|
+
hops.assertInvariants();
|
|
1336
|
+
});
|
|
1337
|
+
test("doubleKnockScoped() preserves maxRedirection across challenge redirects", async (t) => {
|
|
1338
|
+
for (const maxRedirection of [0, 1]) await t.step(`limit ${maxRedirection}`, async () => {
|
|
1339
|
+
const url = "https://example.com/lifecycle/challenge-limit";
|
|
1340
|
+
const hops = recordHops((request, index) => index % 2 === 1 ? {
|
|
1341
|
+
status: 401,
|
|
1342
|
+
headers: { "Accept-Signature": "challenge=(\"@method\");created" },
|
|
1343
|
+
body: "Challenge"
|
|
1344
|
+
} : {
|
|
1345
|
+
status: 307,
|
|
1346
|
+
headers: { Location: `${request.url}/next` },
|
|
1347
|
+
body: "Redirect"
|
|
1348
|
+
});
|
|
1349
|
+
await assertRejects(() => doubleKnockScoped(new Request(url, {
|
|
1350
|
+
method: "POST",
|
|
1351
|
+
body: "activity"
|
|
1352
|
+
}), lifecycleIdentity, { maxRedirection }, hops.openHop, (response) => response.text()), FetchError, `Too many redirections (${maxRedirection + 1})`);
|
|
1353
|
+
assertEquals(hops.requests.length, 2 * (maxRedirection + 1));
|
|
1354
|
+
assertEquals(hops.events.filter((event) => event.startsWith("open ")).length, maxRedirection + 1);
|
|
1355
|
+
assertEquals(hops.events.filter((event) => event.startsWith("cancel #")).length, hops.requests.length);
|
|
1356
|
+
hops.assertInvariants();
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
test("doubleKnockScoped() detects loops after challenge redirects", async () => {
|
|
1360
|
+
const url = "https://example.com/lifecycle/challenge-loop";
|
|
1361
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1362
|
+
status: 401,
|
|
1363
|
+
headers: { "Accept-Signature": "challenge=(\"@method\");created" },
|
|
1364
|
+
body: "Challenge"
|
|
1365
|
+
} : {
|
|
1366
|
+
status: 307,
|
|
1367
|
+
headers: { Location: url },
|
|
1368
|
+
body: "Redirect"
|
|
1369
|
+
});
|
|
1370
|
+
await assertRejects(() => doubleKnockScoped(new Request(url, {
|
|
1371
|
+
method: "POST",
|
|
1372
|
+
body: "activity"
|
|
1373
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), FetchError, "Redirect loop detected");
|
|
1374
|
+
assertEquals(hops.requests.length, 2);
|
|
1375
|
+
assertEquals(hops.events, [
|
|
1376
|
+
`open ${url}`,
|
|
1377
|
+
`fetch #1 ${url}`,
|
|
1378
|
+
"cancel #1",
|
|
1379
|
+
`fetch #2 ${url}`,
|
|
1380
|
+
"cancel #2",
|
|
1381
|
+
`close ${url}`
|
|
1382
|
+
]);
|
|
1383
|
+
hops.assertInvariants();
|
|
1384
|
+
});
|
|
1385
|
+
test("doubleKnockScoped() retries another spec on the same hop", async () => {
|
|
1386
|
+
const url = "https://example.com/lifecycle/retry";
|
|
1387
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1388
|
+
status: 401,
|
|
1389
|
+
body: "Unauthorized"
|
|
1390
|
+
} : {
|
|
1391
|
+
status: 202,
|
|
1392
|
+
body: "Accepted"
|
|
1393
|
+
});
|
|
1394
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1395
|
+
method: "POST",
|
|
1396
|
+
body: "activity"
|
|
1397
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), "Accepted");
|
|
1398
|
+
assertEquals(hops.events, [
|
|
1399
|
+
`open ${url}`,
|
|
1400
|
+
`fetch #1 ${url}`,
|
|
1401
|
+
"cancel #1",
|
|
1402
|
+
`fetch #2 ${url}`,
|
|
1403
|
+
"consumed #2",
|
|
1404
|
+
`close ${url}`
|
|
1405
|
+
]);
|
|
1406
|
+
assertEquals(hops.requests.map((r) => r.headers.has("Signature-Input")), [true, false]);
|
|
1407
|
+
hops.assertInvariants();
|
|
1408
|
+
});
|
|
1409
|
+
test("doubleKnockScoped() closes the retried hop before a redirect", async () => {
|
|
1410
|
+
const url = "https://example.com/lifecycle/retry-redirect";
|
|
1411
|
+
const next = "https://example.com/lifecycle/final";
|
|
1412
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1413
|
+
status: 401,
|
|
1414
|
+
body: "Unauthorized"
|
|
1415
|
+
} : index === 2 ? {
|
|
1416
|
+
status: 302,
|
|
1417
|
+
headers: { Location: next },
|
|
1418
|
+
body: "Found"
|
|
1419
|
+
} : {
|
|
1420
|
+
status: 200,
|
|
1421
|
+
body: "Done"
|
|
1422
|
+
});
|
|
1423
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1424
|
+
method: "POST",
|
|
1425
|
+
body: "activity"
|
|
1426
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), "Done");
|
|
1427
|
+
assertEquals(hops.events, [
|
|
1428
|
+
`open ${url}`,
|
|
1429
|
+
`fetch #1 ${url}`,
|
|
1430
|
+
"cancel #1",
|
|
1431
|
+
`fetch #2 ${url}`,
|
|
1432
|
+
"cancel #2",
|
|
1433
|
+
`close ${url}`,
|
|
1434
|
+
`open ${next}`,
|
|
1435
|
+
`fetch #3 ${next}`,
|
|
1436
|
+
"consumed #3",
|
|
1437
|
+
`close ${next}`
|
|
1438
|
+
]);
|
|
1439
|
+
assertEquals(await hops.requests[2].text(), "activity");
|
|
1440
|
+
hops.assertInvariants();
|
|
1441
|
+
});
|
|
1442
|
+
test("doubleKnockScoped() retries transport errors on the same hop", async (t) => {
|
|
1443
|
+
await t.step("GET", async () => {
|
|
1444
|
+
const url = "https://example.com/lifecycle/flaky";
|
|
1445
|
+
const hops = recordHops((_, index) => {
|
|
1446
|
+
if (index === 1) throw new TypeError("connection reset");
|
|
1447
|
+
return { body: "Success" };
|
|
1448
|
+
});
|
|
1449
|
+
assertEquals(await doubleKnockScoped(new Request(url), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), "Success");
|
|
1450
|
+
assertEquals(hops.events, [
|
|
1451
|
+
`open ${url}`,
|
|
1452
|
+
`fetch #1 ${url}`,
|
|
1453
|
+
`fetch #2 ${url}`,
|
|
1454
|
+
"consumed #2",
|
|
1455
|
+
`close ${url}`
|
|
1456
|
+
]);
|
|
1457
|
+
hops.assertInvariants();
|
|
1458
|
+
});
|
|
1459
|
+
await t.step("POST", async () => {
|
|
1460
|
+
const url = "https://example.com/lifecycle/flaky-inbox";
|
|
1461
|
+
const hops = recordHops(() => {
|
|
1462
|
+
throw new TypeError("connection reset");
|
|
1463
|
+
});
|
|
1464
|
+
await assertRejects(() => doubleKnockScoped(new Request(url, {
|
|
1465
|
+
method: "POST",
|
|
1466
|
+
body: "activity"
|
|
1467
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => response.text()), FetchError);
|
|
1468
|
+
assertEquals(hops.events, [
|
|
1469
|
+
`open ${url}`,
|
|
1470
|
+
`fetch #1 ${url}`,
|
|
1471
|
+
`close ${url}`
|
|
1472
|
+
]);
|
|
1473
|
+
hops.assertInvariants();
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
async function captureLogs(fn) {
|
|
1477
|
+
const records = [];
|
|
1478
|
+
await configure({
|
|
1479
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1480
|
+
loggers: [{
|
|
1481
|
+
category: "fedify",
|
|
1482
|
+
sinks: ["buffer"],
|
|
1483
|
+
lowestLevel: "debug"
|
|
1484
|
+
}, {
|
|
1485
|
+
category: ["logtape", "meta"],
|
|
1486
|
+
sinks: []
|
|
1487
|
+
}],
|
|
1488
|
+
reset: true
|
|
1489
|
+
});
|
|
1490
|
+
try {
|
|
1491
|
+
await fn();
|
|
1492
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1493
|
+
} finally {
|
|
1494
|
+
await reset();
|
|
1495
|
+
}
|
|
1496
|
+
return records;
|
|
1497
|
+
}
|
|
1498
|
+
test("doubleKnockScoped() logs a redirect body failing to cancel and follows the redirect", async () => {
|
|
1499
|
+
const url = "https://example.com/lifecycle/cancel-failure";
|
|
1500
|
+
const next = "https://example.com/lifecycle/cancel-failure-final";
|
|
1501
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1502
|
+
status: 302,
|
|
1503
|
+
headers: { Location: next },
|
|
1504
|
+
body: "Found",
|
|
1505
|
+
failCancel: true
|
|
1506
|
+
} : {
|
|
1507
|
+
status: 200,
|
|
1508
|
+
body: "Done"
|
|
1509
|
+
});
|
|
1510
|
+
let result;
|
|
1511
|
+
const records = await captureLogs(async () => {
|
|
1512
|
+
result = await doubleKnockScoped(new Request(url), lifecycleIdentity, {}, hops.openHop, (response) => response.text());
|
|
1513
|
+
});
|
|
1514
|
+
assertEquals(result, "Done");
|
|
1515
|
+
assertEquals(hops.events, [
|
|
1516
|
+
`open ${url}`,
|
|
1517
|
+
`fetch #1 ${url}`,
|
|
1518
|
+
"cancel #1",
|
|
1519
|
+
`close ${url}`,
|
|
1520
|
+
`open ${next}`,
|
|
1521
|
+
`fetch #2 ${next}`,
|
|
1522
|
+
"consumed #2",
|
|
1523
|
+
`close ${next}`
|
|
1524
|
+
]);
|
|
1525
|
+
const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
|
|
1526
|
+
assertEquals(failures.map((r) => [r.level, r.properties.url]), [["debug", url]]);
|
|
1527
|
+
assertEquals(failures[0].properties.error.message, "cancel #1 failed");
|
|
1528
|
+
hops.assertInvariants();
|
|
1529
|
+
});
|
|
1530
|
+
test("doubleKnockScoped() is not blocked by a body cancellation that never settles", async () => {
|
|
1531
|
+
const url = "https://example.com/lifecycle/hanging-cancel";
|
|
1532
|
+
const next = "https://example.com/lifecycle/hanging-cancel-final";
|
|
1533
|
+
const hops = recordHops((_, index) => index === 1 ? {
|
|
1534
|
+
status: 401,
|
|
1535
|
+
body: "Unauthorized",
|
|
1536
|
+
hangCancel: true
|
|
1537
|
+
} : index === 2 ? {
|
|
1538
|
+
status: 302,
|
|
1539
|
+
headers: { Location: next },
|
|
1540
|
+
body: "Found",
|
|
1541
|
+
hangCancel: true
|
|
1542
|
+
} : {
|
|
1543
|
+
status: 200,
|
|
1544
|
+
body: "Done",
|
|
1545
|
+
hangCancel: true
|
|
1546
|
+
});
|
|
1547
|
+
assertEquals(await doubleKnockScoped(new Request(url, {
|
|
1548
|
+
method: "POST",
|
|
1549
|
+
body: "activity"
|
|
1550
|
+
}), lifecycleIdentity, {}, hops.openHop, (response) => Promise.resolve(response.status)), 200);
|
|
1551
|
+
assertEquals(hops.events, [
|
|
1552
|
+
`open ${url}`,
|
|
1553
|
+
`fetch #1 ${url}`,
|
|
1554
|
+
"cancel #1",
|
|
1555
|
+
`fetch #2 ${url}`,
|
|
1556
|
+
"cancel #2",
|
|
1557
|
+
`close ${url}`,
|
|
1558
|
+
`open ${next}`,
|
|
1559
|
+
`fetch #3 ${next}`,
|
|
1560
|
+
"cancel #3 by close",
|
|
1561
|
+
`close ${next}`
|
|
1562
|
+
]);
|
|
1563
|
+
hops.assertInvariants();
|
|
1564
|
+
});
|
|
1565
|
+
test("doubleKnockScoped() completes with responses cloned by fetch-mock", async () => {
|
|
1566
|
+
esm_default.spyGlobal();
|
|
1567
|
+
try {
|
|
1568
|
+
let attempts = 0;
|
|
1569
|
+
esm_default.post("https://example.com/tee/redirect", () => {
|
|
1570
|
+
attempts++;
|
|
1571
|
+
return attempts === 1 ? new Response("Unauthorized", { status: 401 }) : new Response("Found", {
|
|
1572
|
+
status: 302,
|
|
1573
|
+
headers: { Location: "https://example.com/tee/final" }
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
esm_default.post("https://example.com/tee/final", () => new Response("Accepted", { status: 202 }));
|
|
1577
|
+
const request = () => new Request("https://example.com/tee/redirect", {
|
|
1578
|
+
method: "POST",
|
|
1579
|
+
body: "activity"
|
|
1580
|
+
});
|
|
1581
|
+
assertEquals(await doubleKnockScoped(request(), lifecycleIdentity, {}, openDirectFetch, (response) => Promise.resolve(response.status)), 202);
|
|
1582
|
+
assertEquals(attempts, 2);
|
|
1583
|
+
attempts = 0;
|
|
1584
|
+
const response = await doubleKnock(request(), lifecycleIdentity);
|
|
1585
|
+
assertEquals(response.status, 202);
|
|
1586
|
+
assertEquals(await response.text(), "Accepted");
|
|
1587
|
+
assertEquals(attempts, 2);
|
|
1588
|
+
} finally {
|
|
1589
|
+
esm_default.hardReset();
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
test("doubleKnockScoped() closes the session when consuming fails", async (t) => {
|
|
1593
|
+
const url = "https://example.com/lifecycle/consume";
|
|
1594
|
+
await t.step("exception", async () => {
|
|
1595
|
+
const hops = recordHops(() => ({ body: "x".repeat(1024) }));
|
|
1596
|
+
const error = /* @__PURE__ */ new Error("consume failed");
|
|
1597
|
+
await assertRejects(() => doubleKnockScoped(new Request(url), lifecycleIdentity, {}, hops.openHop, async (response) => {
|
|
1598
|
+
const reader = response.body.getReader();
|
|
1599
|
+
await reader.read();
|
|
1600
|
+
reader.releaseLock();
|
|
1601
|
+
throw error;
|
|
1602
|
+
}), Error, "consume failed");
|
|
1603
|
+
assertEquals(hops.events, [
|
|
1604
|
+
`open ${url}`,
|
|
1605
|
+
`fetch #1 ${url}`,
|
|
1606
|
+
"cancel #1 by close",
|
|
1607
|
+
`close ${url}`
|
|
1608
|
+
]);
|
|
1609
|
+
hops.assertInvariants();
|
|
1610
|
+
});
|
|
1611
|
+
await t.step("abort", async () => {
|
|
1612
|
+
const hops = recordHops(() => ({ body: "Success" }));
|
|
1613
|
+
const controller = new AbortController();
|
|
1614
|
+
const reason = /* @__PURE__ */ new Error("aborted while consuming");
|
|
1615
|
+
assert(await assertRejects(() => doubleKnockScoped(new Request(url), lifecycleIdentity, { signal: controller.signal }, hops.openHop, () => {
|
|
1616
|
+
controller.abort(reason);
|
|
1617
|
+
controller.signal.throwIfAborted();
|
|
1618
|
+
return Promise.resolve();
|
|
1619
|
+
})) === reason);
|
|
1620
|
+
assertEquals(hops.events, [
|
|
1621
|
+
`open ${url}`,
|
|
1622
|
+
`fetch #1 ${url}`,
|
|
1623
|
+
"cancel #1 by close",
|
|
1624
|
+
`close ${url}`
|
|
1625
|
+
]);
|
|
1626
|
+
hops.assertInvariants();
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
test("doubleKnock() returns the final response without cancelling its body", async () => {
|
|
1630
|
+
const url = "https://example.com/lifecycle/public";
|
|
1631
|
+
const next = "https://example.com/lifecycle/public-final";
|
|
1632
|
+
const events = [];
|
|
1633
|
+
const responses = [];
|
|
1634
|
+
const originalFetch = globalThis.fetch;
|
|
1635
|
+
globalThis.fetch = ((input) => {
|
|
1636
|
+
const requestUrl = input instanceof Request ? input.url : String(input);
|
|
1637
|
+
const index = responses.length + 1;
|
|
1638
|
+
events.push(`fetch #${index} ${requestUrl}`);
|
|
1639
|
+
const recorded = createRecordedResponse(events, index, requestUrl === url ? {
|
|
1640
|
+
status: 302,
|
|
1641
|
+
headers: { Location: next },
|
|
1642
|
+
body: "Found"
|
|
1643
|
+
} : {
|
|
1644
|
+
status: 200,
|
|
1645
|
+
body: "Done"
|
|
1646
|
+
});
|
|
1647
|
+
responses.push(recorded);
|
|
1648
|
+
return Promise.resolve(recorded.response);
|
|
1649
|
+
});
|
|
1650
|
+
try {
|
|
1651
|
+
const response = await doubleKnock(new Request(url), lifecycleIdentity);
|
|
1652
|
+
assertEquals(events, [
|
|
1653
|
+
`fetch #1 ${url}`,
|
|
1654
|
+
"cancel #1",
|
|
1655
|
+
`fetch #2 ${next}`
|
|
1656
|
+
]);
|
|
1657
|
+
assertFalse(responses[1].settled());
|
|
1658
|
+
assertEquals(await response.text(), "Done");
|
|
1659
|
+
assertEquals(events.at(-1), "consumed #2");
|
|
1660
|
+
} finally {
|
|
1661
|
+
globalThis.fetch = originalFetch;
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
test("doubleKnock() is not blocked by a body cancellation that never settles", async () => {
|
|
1665
|
+
const url = "https://example.com/lifecycle/public-hanging";
|
|
1666
|
+
const next = "https://example.com/lifecycle/public-hanging-final";
|
|
1667
|
+
const events = [];
|
|
1668
|
+
const originalFetch = globalThis.fetch;
|
|
1669
|
+
let index = 0;
|
|
1670
|
+
globalThis.fetch = ((input) => {
|
|
1671
|
+
const requestUrl = input instanceof Request ? input.url : String(input);
|
|
1672
|
+
index++;
|
|
1673
|
+
events.push(`fetch #${index} ${requestUrl}`);
|
|
1674
|
+
return Promise.resolve(createRecordedResponse(events, index, index === 1 ? {
|
|
1675
|
+
status: 401,
|
|
1676
|
+
body: "Unauthorized",
|
|
1677
|
+
hangCancel: true
|
|
1678
|
+
} : index === 2 ? {
|
|
1679
|
+
status: 302,
|
|
1680
|
+
headers: { Location: next },
|
|
1681
|
+
body: "Found",
|
|
1682
|
+
hangCancel: true
|
|
1683
|
+
} : {
|
|
1684
|
+
status: 200,
|
|
1685
|
+
body: "Done"
|
|
1686
|
+
}).response);
|
|
1687
|
+
});
|
|
1688
|
+
try {
|
|
1689
|
+
const response = await doubleKnock(new Request(url, {
|
|
1690
|
+
method: "POST",
|
|
1691
|
+
body: "activity"
|
|
1692
|
+
}), lifecycleIdentity);
|
|
1693
|
+
assertEquals(events, [
|
|
1694
|
+
`fetch #1 ${url}`,
|
|
1695
|
+
"cancel #1",
|
|
1696
|
+
`fetch #2 ${url}`,
|
|
1697
|
+
"cancel #2",
|
|
1698
|
+
`fetch #3 ${next}`
|
|
1699
|
+
]);
|
|
1700
|
+
assertEquals(await response.text(), "Done");
|
|
1701
|
+
} finally {
|
|
1702
|
+
globalThis.fetch = originalFetch;
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1262
1705
|
test("timingSafeEqual()", async (t) => {
|
|
1263
1706
|
await t.step("should return true for equal empty arrays", () => {
|
|
1264
1707
|
assert(timingSafeEqual(new Uint8Array([]), new Uint8Array([])));
|
|
@@ -1965,5 +2408,88 @@ test("doubleKnock(): Accept-Signature with multiple compatible entries fulfills
|
|
|
1965
2408
|
assertEquals(requestCount, 2);
|
|
1966
2409
|
esm_default.hardReset();
|
|
1967
2410
|
});
|
|
2411
|
+
function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
|
|
2412
|
+
const bytes = new TextEncoder().encode(init.body ?? "");
|
|
2413
|
+
const chunkSize = init.chunkSize ?? 256;
|
|
2414
|
+
let offset = 0;
|
|
2415
|
+
let settled = false;
|
|
2416
|
+
const body = new ReadableStream({
|
|
2417
|
+
pull(controller) {
|
|
2418
|
+
if (offset >= bytes.length) {
|
|
2419
|
+
settled = true;
|
|
2420
|
+
events.push(`consumed #${index}`);
|
|
2421
|
+
controller.close();
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
controller.enqueue(bytes.slice(offset, offset + chunkSize));
|
|
2425
|
+
offset += chunkSize;
|
|
2426
|
+
},
|
|
2427
|
+
cancel() {
|
|
2428
|
+
settled = true;
|
|
2429
|
+
events.push(`cancel #${index}${cancelSuffix()}`);
|
|
2430
|
+
if (init.failCancel) throw new Error(`cancel #${index} failed`);
|
|
2431
|
+
if (init.hangCancel) return new Promise(() => {});
|
|
2432
|
+
}
|
|
2433
|
+
}, { highWaterMark: 0 });
|
|
2434
|
+
return {
|
|
2435
|
+
response: new Response(body, {
|
|
2436
|
+
status: init.status ?? 200,
|
|
2437
|
+
headers: init.headers
|
|
2438
|
+
}),
|
|
2439
|
+
settled: () => settled
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function recordHops(respond) {
|
|
2443
|
+
const events = [];
|
|
2444
|
+
const requests = [];
|
|
2445
|
+
const unsettledAtClose = [];
|
|
2446
|
+
let opens = 0;
|
|
2447
|
+
let closes = 0;
|
|
2448
|
+
let fetches = 0;
|
|
2449
|
+
return {
|
|
2450
|
+
events,
|
|
2451
|
+
requests,
|
|
2452
|
+
openHop(url) {
|
|
2453
|
+
opens++;
|
|
2454
|
+
events.push(`open ${url}`);
|
|
2455
|
+
const tracked = [];
|
|
2456
|
+
let closed = false;
|
|
2457
|
+
let closing = false;
|
|
2458
|
+
return Promise.resolve({
|
|
2459
|
+
url: new URL(url),
|
|
2460
|
+
async fetch(input, init) {
|
|
2461
|
+
if (closed) throw new TypeError("The session is closed.");
|
|
2462
|
+
const request = new Request(input instanceof Request ? input.clone() : input, init);
|
|
2463
|
+
const index = ++fetches;
|
|
2464
|
+
requests.push(request);
|
|
2465
|
+
events.push(`fetch #${index} ${request.url}`);
|
|
2466
|
+
const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
|
|
2467
|
+
tracked.push({
|
|
2468
|
+
...recorded,
|
|
2469
|
+
index
|
|
2470
|
+
});
|
|
2471
|
+
return recorded.response;
|
|
2472
|
+
},
|
|
2473
|
+
close() {
|
|
2474
|
+
if (closed) return Promise.resolve();
|
|
2475
|
+
closed = true;
|
|
2476
|
+
closes++;
|
|
2477
|
+
closing = true;
|
|
2478
|
+
for (const { response, settled, index } of tracked) {
|
|
2479
|
+
if (settled()) continue;
|
|
2480
|
+
if (response.body?.locked) unsettledAtClose.push(index);
|
|
2481
|
+
else response.body?.cancel().catch(() => {});
|
|
2482
|
+
}
|
|
2483
|
+
events.push(`close ${url}`);
|
|
2484
|
+
return Promise.resolve();
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2487
|
+
},
|
|
2488
|
+
assertInvariants() {
|
|
2489
|
+
assertEquals(opens, closes);
|
|
2490
|
+
assertEquals(unsettledAtClose, []);
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
1968
2494
|
//#endregion
|
|
1969
2495
|
export {};
|