@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
- package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
- package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
- package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
- package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
- package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
- package/dist/{builder-yAlpiIqP.mjs → builder-DSZ7FZmg.mjs} +96 -42
- package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
- package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
- package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
- package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
- package/dist/compat/mod.d.cts +2 -1
- package/dist/compat/mod.d.ts +2 -3
- package/dist/compat/mod.js +3 -3
- package/dist/compat/outgoing-jsonld.test.mjs +4 -4
- package/dist/compat/public-audience.test.mjs +4 -4
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
- package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
- package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
- package/dist/{deno-XBVlKnOX.mjs → deno-DCdM7d93.mjs} +1 -1
- package/dist/{docloader-DPp-X6gk.mjs → docloader-B7mVwA5_.mjs} +4 -4
- package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
- package/dist/federation/builder.test.mjs +163 -11
- package/dist/federation/circuit-breaker.test.d.mts +2 -0
- package/dist/federation/circuit-breaker.test.mjs +446 -0
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +1770 -29
- package/dist/federation/idempotency.test.mjs +5 -5
- package/dist/federation/inbox.test.mjs +4 -4
- package/dist/federation/keycache.test.mjs +5 -5
- package/dist/federation/kv.test.mjs +2 -2
- package/dist/federation/metrics.test.d.mts +2 -0
- package/dist/federation/metrics.test.mjs +634 -0
- package/dist/federation/middleware.test.mjs +4036 -146
- package/dist/federation/mod.cjs +195 -14
- package/dist/federation/mod.d.cts +6 -4
- package/dist/federation/mod.d.ts +6 -6
- package/dist/federation/mod.js +192 -14
- package/dist/federation/mq.test.mjs +176 -15
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/retry.test.mjs +3 -3
- package/dist/federation/router.test.mjs +190 -9
- package/dist/federation/send.test.mjs +153 -15
- package/dist/federation/temporal.test.d.mts +2 -0
- package/dist/federation/temporal.test.mjs +71 -0
- package/dist/federation/webfinger.test.mjs +150 -5
- package/dist/{http-CSlLA54v.mjs → http-BufbkLuW.mjs} +146 -47
- package/dist/{http-B4rrzxtg.js → http-Dh18nwJg.js} +994 -64
- package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
- package/dist/{http-BrGqJDXL.cjs → http-gOiudB3g.cjs} +1099 -61
- package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
- package/dist/{key-Bzx--sHD.mjs → key-DZdKLJXT.mjs} +43 -18
- package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
- package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BuK_nlpY.cjs} +20 -3
- package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-C7SQnkGI.mjs} +21 -3
- package/dist/{kv-cache-EMIqoIuB.js → kv-cache-DNDSb6Po.js} +20 -3
- package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
- package/dist/ld-0Lsznacf.mjs +626 -0
- package/dist/metrics-BHeWd24f.mjs +815 -0
- package/dist/{middleware-zjZ6AFCW.mjs → middleware-BNANvDh2.mjs} +1 -1
- package/dist/{middleware-nidH7VZH.js → middleware-BgzRkDcm.js} +2260 -556
- package/dist/{middleware-An4DjES4.cjs → middleware-D-iES1nQ.cjs} +2318 -623
- package/dist/{middleware-K1g6bEkV.mjs → middleware-EV6J_eAc.mjs} +1639 -373
- package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
- package/dist/mod-C0F6kvgS.d.cts +182 -0
- package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
- package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
- package/dist/mod-vPYVoa5n.d.ts +182 -0
- package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
- package/dist/mod.cjs +9 -6
- package/dist/mod.d.cts +12 -10
- package/dist/mod.d.ts +12 -12
- package/dist/mod.js +11 -11
- package/dist/mq-D-nlpY04.d.ts +208 -0
- package/dist/mq-D8uSFzxe.d.cts +208 -0
- package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
- package/dist/nodeinfo/client.test.mjs +4 -4
- package/dist/nodeinfo/handler.test.mjs +4 -4
- package/dist/nodeinfo/mod.d.cts +2 -1
- package/dist/nodeinfo/mod.d.ts +2 -3
- package/dist/nodeinfo/mod.js +3 -3
- package/dist/nodeinfo/types.test.mjs +3 -3
- package/dist/otel/exporter.test.mjs +28 -25
- package/dist/otel/mod.cjs +6 -5
- package/dist/otel/mod.d.cts +5 -3
- package/dist/otel/mod.d.ts +5 -5
- package/dist/otel/mod.js +8 -7
- package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
- package/dist/{owner-C-tqfvxn.mjs → owner-Cl-1cAR8.mjs} +2 -2
- package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
- package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
- package/dist/{proof-Bbbwf8_x.js → proof-CyXndf6-.js} +378 -15
- package/dist/{proof-BlmRPzrK.mjs → proof-Dnz8jtiQ.mjs} +26 -8
- package/dist/{proof-BW89QMVB.cjs → proof-tz35unOj.cjs} +432 -15
- package/dist/runtime/mod.d.cts +1 -0
- package/dist/runtime/mod.d.ts +1 -2
- package/dist/runtime/mod.js +3 -3
- package/dist/{send-05pJLcb6.mjs → send-DIbBYN5P.mjs} +72 -26
- package/dist/sig/http.test.mjs +356 -33
- package/dist/sig/key.test.mjs +103 -6
- package/dist/sig/ld.test.mjs +696 -7
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +4 -3
- package/dist/sig/mod.d.ts +4 -5
- package/dist/sig/mod.js +4 -4
- package/dist/sig/owner.test.mjs +6 -6
- package/dist/sig/proof.test.mjs +169 -8
- package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
- package/dist/temporal-BEwob1Vg.mjs +95 -0
- package/dist/testing/mod.d.mts +110 -10
- package/dist/testing/mod.mjs +1 -2
- package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
- package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
- package/dist/utils/docloader.test.mjs +6 -6
- package/dist/utils/kv-cache.test.mjs +67 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +2 -1
- package/dist/utils/mod.d.ts +2 -3
- package/dist/utils/mod.js +3 -3
- package/dist/vocab/cjs.test.mjs +1 -1
- package/dist/vocab/mod.d.cts +1 -0
- package/dist/vocab/mod.d.ts +1 -2
- package/dist/vocab/mod.js +2 -2
- package/package.json +16 -16
- package/dist/ld-DR78beiv.mjs +0 -279
- package/dist/middleware-BFBaR0mF.cjs +0 -4
- package/dist/mod-2d12ffz3.d.ts +0 -64
- package/dist/mod-D35TRn09.d.cts +0 -62
- package/dist/router-CrMLXoOr.mjs +0 -114
- package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
- package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
- package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
- package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
- package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
- package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
- /package/dist/{retry-bMXBL97A.mjs → retry-CXg_MBI-.mjs} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
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 { n as
|
|
6
|
-
import {
|
|
4
|
+
import { n as version, t as name } from "./deno-DCdM7d93.mjs";
|
|
5
|
+
import { n as getDurationMs, r as getFederationMetrics } from "./metrics-BHeWd24f.mjs";
|
|
6
|
+
import { n as doubleKnock } from "./http-BufbkLuW.mjs";
|
|
7
7
|
import { getLogger } from "@logtape/logtape";
|
|
8
|
+
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
9
|
+
import { FetchError } from "@fedify/vocab-runtime";
|
|
8
10
|
//#region src/federation/send.ts
|
|
9
11
|
/**
|
|
10
12
|
* Extracts the inbox URLs from recipients.
|
|
@@ -64,6 +66,25 @@ function sendActivity(options) {
|
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
|
|
69
|
+
function getActivityActorId(activity) {
|
|
70
|
+
if (!isRecord(activity)) return void 0;
|
|
71
|
+
return getIdValue(activity.actor);
|
|
72
|
+
}
|
|
73
|
+
function getIdValue(value) {
|
|
74
|
+
if (typeof value === "string" && value !== "") return value;
|
|
75
|
+
if (value instanceof URL) return value.href;
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
for (const item of value) {
|
|
78
|
+
const id = getIdValue(item);
|
|
79
|
+
if (id != null) return id;
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (isRecord(value)) return getIdValue(value.id);
|
|
84
|
+
}
|
|
85
|
+
function isRecord(value) {
|
|
86
|
+
return typeof value === "object" && value != null;
|
|
87
|
+
}
|
|
67
88
|
async function readLimitedResponseBody(response, maxBytes) {
|
|
68
89
|
if (response.body == null) return "";
|
|
69
90
|
const reader = response.body.getReader();
|
|
@@ -91,12 +112,15 @@ async function readLimitedResponseBody(response, maxBytes) {
|
|
|
91
112
|
if (truncated) result += "… (truncated)";
|
|
92
113
|
return result;
|
|
93
114
|
}
|
|
94
|
-
async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider }, span) {
|
|
115
|
+
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
95
116
|
const logger = getLogger([
|
|
96
117
|
"fedify",
|
|
97
118
|
"federation",
|
|
98
119
|
"outbox"
|
|
99
120
|
]);
|
|
121
|
+
const federationMetrics = getFederationMetrics(meterProvider);
|
|
122
|
+
const started = performance.now();
|
|
123
|
+
let deliverySuccess = false;
|
|
100
124
|
headers = new Headers(headers);
|
|
101
125
|
headers.set("Content-Type", "application/activity+json");
|
|
102
126
|
const request = new Request(inbox, {
|
|
@@ -123,34 +147,49 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
|
|
|
123
147
|
specDeterminer
|
|
124
148
|
});
|
|
125
149
|
} catch (error) {
|
|
150
|
+
const transportError = error instanceof FetchError ? error : createFetchError(inbox.href, error);
|
|
126
151
|
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
127
152
|
activityId,
|
|
128
153
|
inbox: inbox.href,
|
|
129
|
-
error
|
|
154
|
+
error: transportError
|
|
130
155
|
});
|
|
131
|
-
|
|
156
|
+
federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
|
|
157
|
+
throw transportError;
|
|
132
158
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
try {
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
let error;
|
|
162
|
+
try {
|
|
163
|
+
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
164
|
+
} catch (_) {
|
|
165
|
+
error = "";
|
|
166
|
+
}
|
|
167
|
+
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
168
|
+
activityId,
|
|
169
|
+
inbox: inbox.href,
|
|
170
|
+
status: response.status,
|
|
171
|
+
statusText: response.statusText,
|
|
172
|
+
error
|
|
173
|
+
});
|
|
174
|
+
throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
|
|
139
175
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
inbox: inbox.href,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
deliverySuccess = true;
|
|
177
|
+
const eventAttributes = {
|
|
178
|
+
"activitypub.inbox.url": inbox.href,
|
|
179
|
+
"activitypub.activity.id": activityId ?? ""
|
|
180
|
+
};
|
|
181
|
+
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
182
|
+
const actorId = getActivityActorId(activity);
|
|
183
|
+
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
184
|
+
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
185
|
+
} finally {
|
|
186
|
+
federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
|
|
148
187
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
188
|
+
}
|
|
189
|
+
function createFetchError(url, cause) {
|
|
190
|
+
const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
|
|
191
|
+
error.cause = cause;
|
|
192
|
+
return error;
|
|
154
193
|
}
|
|
155
194
|
/**
|
|
156
195
|
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
@@ -175,18 +214,25 @@ var SendActivityError = class extends Error {
|
|
|
175
214
|
*/
|
|
176
215
|
responseBody;
|
|
177
216
|
/**
|
|
217
|
+
* The response headers from the inbox.
|
|
218
|
+
* @since 2.3.0
|
|
219
|
+
*/
|
|
220
|
+
responseHeaders;
|
|
221
|
+
/**
|
|
178
222
|
* Creates a new {@link SendActivityError}.
|
|
179
223
|
* @param inbox The inbox URL.
|
|
180
224
|
* @param statusCode The HTTP status code.
|
|
181
225
|
* @param message The error message.
|
|
182
226
|
* @param responseBody The response body.
|
|
227
|
+
* @param responseHeaders The response headers.
|
|
183
228
|
*/
|
|
184
|
-
constructor(inbox, statusCode, message, responseBody) {
|
|
229
|
+
constructor(inbox, statusCode, message, responseBody, responseHeaders) {
|
|
185
230
|
super(message);
|
|
186
231
|
this.name = "SendActivityError";
|
|
187
232
|
this.inbox = inbox;
|
|
188
233
|
this.statusCode = statusCode;
|
|
189
234
|
this.responseBody = responseBody;
|
|
235
|
+
this.responseHeaders = new Headers(responseHeaders);
|
|
190
236
|
}
|
|
191
237
|
};
|
|
192
238
|
//#endregion
|
package/dist/sig/http.test.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { t as esm_default } from "../esm-
|
|
5
|
-
import { t as assertEquals } from "../assert_equals-
|
|
6
|
-
import {
|
|
7
|
-
import { n as assertFalse, t as assertRejects } from "../assert_rejects-
|
|
8
|
-
import { t as assertThrows } from "../assert_throws-
|
|
9
|
-
import { t as assert } from "../assert-
|
|
10
|
-
import { t as exportJwk } from "../key-
|
|
11
|
-
import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-
|
|
4
|
+
import { t as esm_default } from "../esm-vrlUxr60.mjs";
|
|
5
|
+
import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
|
|
6
|
+
import { r as assertExists, t as assertStringIncludes } from "../std__assert-BBjXFNOb.mjs";
|
|
7
|
+
import { n as assertGreaterOrEqual, r as assertFalse, t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
|
|
8
|
+
import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
|
|
9
|
+
import { t as assert } from "../assert-OguE97r2.mjs";
|
|
10
|
+
import { t as exportJwk } from "../key-DZdKLJXT.mjs";
|
|
11
|
+
import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-BufbkLuW.mjs";
|
|
12
12
|
import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-DGu1NFwu.mjs";
|
|
13
|
-
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
13
|
+
import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
14
14
|
import { FetchError, exportSpki } from "@fedify/vocab-runtime";
|
|
15
15
|
import { encodeBase64 } from "byte-encodings/base64";
|
|
16
16
|
//#region src/sig/http.test.ts
|
|
@@ -230,6 +230,216 @@ test("verifyRequestDetailed() records failure details on span", async () => {
|
|
|
230
230
|
assertEquals(span.attributes["http_signatures.key_id"], keyId.href);
|
|
231
231
|
assertEquals(span.attributes["http_signatures.key_fetch_status"], 410);
|
|
232
232
|
});
|
|
233
|
+
test("verifyRequestDetailed() records verification duration metric", async (t) => {
|
|
234
|
+
const buildSignedRequest = () => signRequest(new Request("https://example.com/inbox", {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
"Content-Type": "application/activity+json",
|
|
238
|
+
accept: "application/ld+json"
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
242
|
+
type: "Create",
|
|
243
|
+
actor: "https://example.com/key2"
|
|
244
|
+
})
|
|
245
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"));
|
|
246
|
+
await t.step("verified path emits one measurement", async () => {
|
|
247
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
248
|
+
assert((await verifyRequestDetailed(await buildSignedRequest(), {
|
|
249
|
+
contextLoader: mockDocumentLoader,
|
|
250
|
+
documentLoader: mockDocumentLoader,
|
|
251
|
+
meterProvider
|
|
252
|
+
})).verified);
|
|
253
|
+
const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
|
|
254
|
+
assertEquals(measurements.length, 1);
|
|
255
|
+
const measurement = measurements[0];
|
|
256
|
+
assertEquals(measurement.type, "histogram");
|
|
257
|
+
assertGreaterOrEqual(measurement.value, 0);
|
|
258
|
+
assertEquals(measurement.attributes["activitypub.signature.kind"], "http");
|
|
259
|
+
assertEquals(measurement.attributes["activitypub.signature.result"], "verified");
|
|
260
|
+
assertEquals(measurement.attributes["http_signatures.algorithm"], "rsa-sha256");
|
|
261
|
+
assertFalse("http_signatures.failure_reason" in measurement.attributes);
|
|
262
|
+
const keyLookups = recorder.getMeasurements("activitypub.key.lookup");
|
|
263
|
+
assertEquals(keyLookups.length, 1);
|
|
264
|
+
assertEquals(keyLookups[0].attributes["activitypub.lookup.kind"], "public_key");
|
|
265
|
+
assertEquals(keyLookups[0].attributes["activitypub.lookup.result"], "fetched");
|
|
266
|
+
});
|
|
267
|
+
await t.step("missing signature is recorded as result=missing", async () => {
|
|
268
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
269
|
+
const result = await verifyRequestDetailed(new Request("https://example.com/inbox", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
272
|
+
body: "{}"
|
|
273
|
+
}), {
|
|
274
|
+
contextLoader: mockDocumentLoader,
|
|
275
|
+
documentLoader: mockDocumentLoader,
|
|
276
|
+
meterProvider
|
|
277
|
+
});
|
|
278
|
+
assertFalse(result.verified);
|
|
279
|
+
assertEquals(result.reason.type, "noSignature");
|
|
280
|
+
const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
|
|
281
|
+
assertEquals(measurements.length, 1);
|
|
282
|
+
assertEquals(measurements[0].attributes["activitypub.signature.kind"], "http");
|
|
283
|
+
assertEquals(measurements[0].attributes["activitypub.signature.result"], "missing");
|
|
284
|
+
assertFalse("http_signatures.failure_reason" in measurements[0].attributes);
|
|
285
|
+
});
|
|
286
|
+
await t.step("invalid signature is recorded as result=rejected with failure_reason", async () => {
|
|
287
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
288
|
+
const result = await verifyRequestDetailed(new Request("https://example.com/", {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
Date: "Tue, 05 Mar 2024 07:49:44 GMT",
|
|
292
|
+
Digest: "sha-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
|
|
293
|
+
Signature: "keyId=\"https://example.com/key2\",headers=\"(request-target) date digest\",signature=\"AAAA\""
|
|
294
|
+
},
|
|
295
|
+
body: ""
|
|
296
|
+
}), {
|
|
297
|
+
documentLoader: mockDocumentLoader,
|
|
298
|
+
contextLoader: mockDocumentLoader,
|
|
299
|
+
meterProvider
|
|
300
|
+
});
|
|
301
|
+
assertFalse(result.verified);
|
|
302
|
+
assertEquals(result.reason.type, "invalidSignature");
|
|
303
|
+
const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
|
|
304
|
+
assertEquals(measurements.length, 1);
|
|
305
|
+
assertEquals(measurements[0].attributes["activitypub.signature.kind"], "http");
|
|
306
|
+
assertEquals(measurements[0].attributes["activitypub.signature.result"], "rejected");
|
|
307
|
+
assertEquals(measurements[0].attributes["http_signatures.failure_reason"], "invalidSignature");
|
|
308
|
+
});
|
|
309
|
+
await t.step("key fetch failure is recorded as result=rejected with failure_reason=keyFetchError", async () => {
|
|
310
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
311
|
+
const keyId = new URL("https://gone.example/actors/alice#main-key");
|
|
312
|
+
const result = await verifyRequestDetailed(await signRequest(new Request("https://example.com/inbox", {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: {
|
|
315
|
+
"Content-Type": "application/activity+json",
|
|
316
|
+
accept: "application/ld+json"
|
|
317
|
+
},
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
320
|
+
type: "Create",
|
|
321
|
+
actor: "https://gone.example/actors/alice"
|
|
322
|
+
})
|
|
323
|
+
}), rsaPrivateKey2, keyId), {
|
|
324
|
+
contextLoader: mockDocumentLoader,
|
|
325
|
+
documentLoader(url) {
|
|
326
|
+
if (url === keyId.href) throw new FetchError(keyId, `HTTP 410: ${keyId.href}`, new Response(null, { status: 410 }));
|
|
327
|
+
return mockDocumentLoader(url);
|
|
328
|
+
},
|
|
329
|
+
meterProvider
|
|
330
|
+
});
|
|
331
|
+
assertFalse(result.verified);
|
|
332
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
333
|
+
const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
|
|
334
|
+
assertEquals(measurements.length, 1);
|
|
335
|
+
assertEquals(measurements[0].attributes["activitypub.signature.result"], "rejected");
|
|
336
|
+
assertEquals(measurements[0].attributes["http_signatures.failure_reason"], "keyFetchError");
|
|
337
|
+
});
|
|
338
|
+
await t.step("verifyRequest() wrapper emits exactly one measurement, not two", async () => {
|
|
339
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
340
|
+
assertExists(await verifyRequest(await buildSignedRequest(), {
|
|
341
|
+
contextLoader: mockDocumentLoader,
|
|
342
|
+
documentLoader: mockDocumentLoader,
|
|
343
|
+
meterProvider
|
|
344
|
+
}));
|
|
345
|
+
assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1);
|
|
346
|
+
});
|
|
347
|
+
await t.step("cached-key retry emits one measurement, not two", async () => {
|
|
348
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
349
|
+
const cache = { "https://example.com/key2": rsaPublicKey1 };
|
|
350
|
+
assertExists(await verifyRequest(await buildSignedRequest(), {
|
|
351
|
+
contextLoader: mockDocumentLoader,
|
|
352
|
+
documentLoader: mockDocumentLoader,
|
|
353
|
+
meterProvider,
|
|
354
|
+
keyCache: {
|
|
355
|
+
get(keyId) {
|
|
356
|
+
return Promise.resolve(cache[keyId.href]);
|
|
357
|
+
},
|
|
358
|
+
set(keyId, k) {
|
|
359
|
+
cache[keyId.href] = k;
|
|
360
|
+
return Promise.resolve();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}));
|
|
364
|
+
assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1);
|
|
365
|
+
});
|
|
366
|
+
await t.step("key fetch records result=fetched on a cold cache", async () => {
|
|
367
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
368
|
+
assertExists(await verifyRequest(await buildSignedRequest(), {
|
|
369
|
+
contextLoader: mockDocumentLoader,
|
|
370
|
+
documentLoader: mockDocumentLoader,
|
|
371
|
+
meterProvider
|
|
372
|
+
}));
|
|
373
|
+
const measurements = recorder.getMeasurements("activitypub.signature.key_fetch.duration");
|
|
374
|
+
assertEquals(measurements.length, 1);
|
|
375
|
+
assertEquals(measurements[0].type, "histogram");
|
|
376
|
+
assertGreaterOrEqual(measurements[0].value, 0);
|
|
377
|
+
assertEquals(measurements[0].attributes["activitypub.signature.kind"], "http");
|
|
378
|
+
assertEquals(measurements[0].attributes["activitypub.signature.key_fetch.result"], "fetched");
|
|
379
|
+
});
|
|
380
|
+
await t.step("key fetch records result=hit when served from the key cache", async () => {
|
|
381
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
382
|
+
const cache = { "https://example.com/key2": rsaPublicKey2 };
|
|
383
|
+
assertExists(await verifyRequest(await buildSignedRequest(), {
|
|
384
|
+
contextLoader: mockDocumentLoader,
|
|
385
|
+
documentLoader: mockDocumentLoader,
|
|
386
|
+
meterProvider,
|
|
387
|
+
keyCache: {
|
|
388
|
+
get(keyId) {
|
|
389
|
+
return Promise.resolve(cache[keyId.href]);
|
|
390
|
+
},
|
|
391
|
+
set(keyId, k) {
|
|
392
|
+
cache[keyId.href] = k;
|
|
393
|
+
return Promise.resolve();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}));
|
|
397
|
+
const measurements = recorder.getMeasurements("activitypub.signature.key_fetch.duration");
|
|
398
|
+
assertEquals(measurements.length, 1);
|
|
399
|
+
assertEquals(measurements[0].attributes["activitypub.signature.key_fetch.result"], "hit");
|
|
400
|
+
});
|
|
401
|
+
await t.step("key fetch records result=error when the remote key returns HTTP 410", async () => {
|
|
402
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
403
|
+
const keyId = new URL("https://gone.example/actors/alice#main-key");
|
|
404
|
+
assertFalse((await verifyRequestDetailed(await signRequest(new Request("https://example.com/inbox", {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: {
|
|
407
|
+
"Content-Type": "application/activity+json",
|
|
408
|
+
accept: "application/ld+json"
|
|
409
|
+
},
|
|
410
|
+
body: "{}"
|
|
411
|
+
}), rsaPrivateKey2, keyId), {
|
|
412
|
+
contextLoader: mockDocumentLoader,
|
|
413
|
+
documentLoader(url) {
|
|
414
|
+
if (url === keyId.href) throw new FetchError(keyId, `HTTP 410: ${keyId.href}`, new Response(null, { status: 410 }));
|
|
415
|
+
return mockDocumentLoader(url);
|
|
416
|
+
},
|
|
417
|
+
meterProvider
|
|
418
|
+
})).verified);
|
|
419
|
+
const measurements = recorder.getMeasurements("activitypub.signature.key_fetch.duration");
|
|
420
|
+
assertEquals(measurements.length, 1);
|
|
421
|
+
assertEquals(measurements[0].attributes["activitypub.signature.key_fetch.result"], "error");
|
|
422
|
+
});
|
|
423
|
+
await t.step("draft-cavage with unknown algorithm omits the algorithm metric attribute", async () => {
|
|
424
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
425
|
+
assertFalse((await verifyRequestDetailed(new Request("https://example.com/", {
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: {
|
|
428
|
+
Date: "Tue, 05 Mar 2024 07:49:44 GMT",
|
|
429
|
+
Digest: "sha-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
|
|
430
|
+
Signature: "keyId=\"https://example.com/key2\",algorithm=\"x-attacker-supplied\",headers=\"(request-target) date digest\",signature=\"AAAA\""
|
|
431
|
+
},
|
|
432
|
+
body: ""
|
|
433
|
+
}), {
|
|
434
|
+
documentLoader: mockDocumentLoader,
|
|
435
|
+
contextLoader: mockDocumentLoader,
|
|
436
|
+
meterProvider
|
|
437
|
+
})).verified);
|
|
438
|
+
const measurements = recorder.getMeasurements("activitypub.signature.verification.duration");
|
|
439
|
+
assertEquals(measurements.length, 1);
|
|
440
|
+
assertFalse("http_signatures.algorithm" in measurements[0].attributes);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
233
443
|
test("signRequest() and verifyRequest() [rfc9421] implementation", async () => {
|
|
234
444
|
const currentTimestamp = 1709626184;
|
|
235
445
|
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
@@ -335,15 +545,12 @@ test("createRfc9421SignatureBase()", () => {
|
|
|
335
545
|
].join("\n"));
|
|
336
546
|
});
|
|
337
547
|
test("formatRfc9421Signature()", () => {
|
|
338
|
-
const
|
|
548
|
+
const [signatureInput, signatureHeader] = formatRfc9421Signature(new Uint8Array([
|
|
339
549
|
1,
|
|
340
550
|
2,
|
|
341
551
|
3,
|
|
342
552
|
4
|
|
343
|
-
])
|
|
344
|
-
const keyId = new URL("https://example.com/key");
|
|
345
|
-
const algorithm = "rsa-v1_5-sha256";
|
|
346
|
-
const [signatureInput, signatureHeader] = formatRfc9421Signature(signature, [
|
|
553
|
+
]), [
|
|
347
554
|
{
|
|
348
555
|
"value": "@method",
|
|
349
556
|
params: {}
|
|
@@ -357,8 +564,8 @@ test("formatRfc9421Signature()", () => {
|
|
|
357
564
|
params: {}
|
|
358
565
|
}
|
|
359
566
|
], formatRfc9421SignatureParameters({
|
|
360
|
-
algorithm,
|
|
361
|
-
keyId,
|
|
567
|
+
algorithm: "rsa-v1_5-sha256",
|
|
568
|
+
keyId: new URL("https://example.com/key"),
|
|
362
569
|
created: 1709626184
|
|
363
570
|
}));
|
|
364
571
|
assertEquals(signatureInput, `sig1=("@method" "@target-uri" "host");alg="rsa-v1_5-sha256";keyid="https://example.com/key";created=1709626184`);
|
|
@@ -855,12 +1062,6 @@ test("doubleKnock() function with specDeterminer choosing draft-cavage first", a
|
|
|
855
1062
|
else if (req.headers.has("Signature")) firstSpec = "draft-cavage";
|
|
856
1063
|
return new Response("", { status: 202 });
|
|
857
1064
|
});
|
|
858
|
-
const specDeterminer = {
|
|
859
|
-
determineSpec(_origin) {
|
|
860
|
-
return "draft-cavage-http-signatures-12";
|
|
861
|
-
},
|
|
862
|
-
rememberSpec(_origin, _spec) {}
|
|
863
|
-
};
|
|
864
1065
|
assertEquals((await doubleKnock(new Request("https://example.com/inbox-accepts-any", {
|
|
865
1066
|
method: "POST",
|
|
866
1067
|
body: "Test message with draft-cavage preference",
|
|
@@ -868,7 +1069,12 @@ test("doubleKnock() function with specDeterminer choosing draft-cavage first", a
|
|
|
868
1069
|
}), {
|
|
869
1070
|
keyId: rsaPublicKey2.id,
|
|
870
1071
|
privateKey: rsaPrivateKey2
|
|
871
|
-
}, { specDeterminer
|
|
1072
|
+
}, { specDeterminer: {
|
|
1073
|
+
determineSpec(_origin) {
|
|
1074
|
+
return "draft-cavage-http-signatures-12";
|
|
1075
|
+
},
|
|
1076
|
+
rememberSpec(_origin, _spec) {}
|
|
1077
|
+
} })).status, 202, "Response status should be 202 Accepted");
|
|
872
1078
|
assertEquals(requestCount, 1, "Only one request should have been made");
|
|
873
1079
|
assertEquals(firstSpec, "draft-cavage", "First attempt should use draft-cavage");
|
|
874
1080
|
esm_default.hardReset();
|
|
@@ -979,6 +1185,124 @@ test("doubleKnock() detects redirect loops", async () => {
|
|
|
979
1185
|
assertEquals(requestCount, 2);
|
|
980
1186
|
esm_default.hardReset();
|
|
981
1187
|
});
|
|
1188
|
+
test("doubleKnock() retries idempotent request transport errors", async () => {
|
|
1189
|
+
esm_default.spyGlobal();
|
|
1190
|
+
try {
|
|
1191
|
+
let requestCount = 0;
|
|
1192
|
+
esm_default.get("https://example.com/flaky-document", () => {
|
|
1193
|
+
requestCount++;
|
|
1194
|
+
if (requestCount === 1) throw new TypeError("temporary DNS failure");
|
|
1195
|
+
return new Response("Success", { status: 200 });
|
|
1196
|
+
});
|
|
1197
|
+
const response = await doubleKnock(new Request("https://example.com/flaky-document"), {
|
|
1198
|
+
keyId: rsaPublicKey2.id,
|
|
1199
|
+
privateKey: rsaPrivateKey2
|
|
1200
|
+
});
|
|
1201
|
+
assertEquals(response.status, 200);
|
|
1202
|
+
assertEquals(await response.text(), "Success");
|
|
1203
|
+
assertEquals(requestCount, 2);
|
|
1204
|
+
} finally {
|
|
1205
|
+
esm_default.hardReset();
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
test("doubleKnock() wraps repeated transport errors", async () => {
|
|
1209
|
+
esm_default.spyGlobal();
|
|
1210
|
+
try {
|
|
1211
|
+
let requestCount = 0;
|
|
1212
|
+
const failure = /* @__PURE__ */ new TypeError("DNS lookup failed");
|
|
1213
|
+
esm_default.get("https://example.com/unreachable-document", () => {
|
|
1214
|
+
requestCount++;
|
|
1215
|
+
throw failure;
|
|
1216
|
+
});
|
|
1217
|
+
const request = new Request("https://example.com/unreachable-document");
|
|
1218
|
+
const error = await assertRejects(() => doubleKnock(request, {
|
|
1219
|
+
keyId: rsaPublicKey2.id,
|
|
1220
|
+
privateKey: rsaPrivateKey2
|
|
1221
|
+
}), FetchError, "DNS lookup failed");
|
|
1222
|
+
assertEquals(error.url.href, "https://example.com/unreachable-document");
|
|
1223
|
+
assertEquals(error.cause, failure);
|
|
1224
|
+
assertEquals(requestCount, 2);
|
|
1225
|
+
} finally {
|
|
1226
|
+
esm_default.hardReset();
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
test("doubleKnock() does not retry non-idempotent transport errors", async () => {
|
|
1230
|
+
esm_default.spyGlobal();
|
|
1231
|
+
try {
|
|
1232
|
+
let requestCount = 0;
|
|
1233
|
+
const failure = /* @__PURE__ */ new TypeError("connection reset");
|
|
1234
|
+
esm_default.post("https://example.com/flaky-inbox", () => {
|
|
1235
|
+
requestCount++;
|
|
1236
|
+
throw failure;
|
|
1237
|
+
});
|
|
1238
|
+
const request = new Request("https://example.com/flaky-inbox", {
|
|
1239
|
+
method: "POST",
|
|
1240
|
+
body: "Test activity content",
|
|
1241
|
+
headers: { "Content-Type": "application/activity+json" }
|
|
1242
|
+
});
|
|
1243
|
+
const error = await assertRejects(() => doubleKnock(request, {
|
|
1244
|
+
keyId: rsaPublicKey2.id,
|
|
1245
|
+
privateKey: rsaPrivateKey2
|
|
1246
|
+
}), FetchError, "connection reset");
|
|
1247
|
+
assertEquals(error.url.href, "https://example.com/flaky-inbox");
|
|
1248
|
+
assertEquals(error.cause, failure);
|
|
1249
|
+
assertEquals(requestCount, 1);
|
|
1250
|
+
} finally {
|
|
1251
|
+
esm_default.hardReset();
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
test("doubleKnock() preserves Request signal abort reasons", async () => {
|
|
1255
|
+
const controller = new AbortController();
|
|
1256
|
+
const abortReason = "request aborted";
|
|
1257
|
+
controller.abort(abortReason);
|
|
1258
|
+
const request = new Request("https://example.com/request-abort", { signal: controller.signal });
|
|
1259
|
+
assertEquals(await assertRejects(() => doubleKnock(request, {
|
|
1260
|
+
keyId: rsaPublicKey2.id,
|
|
1261
|
+
privateKey: rsaPrivateKey2
|
|
1262
|
+
})), abortReason);
|
|
1263
|
+
});
|
|
1264
|
+
test("doubleKnock() preserves Request signal aborts during retry delay", async () => {
|
|
1265
|
+
esm_default.spyGlobal();
|
|
1266
|
+
try {
|
|
1267
|
+
let requestCount = 0;
|
|
1268
|
+
const controller = new AbortController();
|
|
1269
|
+
const abortReason = "retry aborted";
|
|
1270
|
+
esm_default.get("https://example.com/aborted-retry", () => {
|
|
1271
|
+
requestCount++;
|
|
1272
|
+
setTimeout(() => controller.abort(abortReason));
|
|
1273
|
+
throw new TypeError("temporary DNS failure");
|
|
1274
|
+
});
|
|
1275
|
+
const request = new Request("https://example.com/aborted-retry", { signal: controller.signal });
|
|
1276
|
+
assertEquals(await assertRejects(() => doubleKnock(request, {
|
|
1277
|
+
keyId: rsaPublicKey2.id,
|
|
1278
|
+
privateKey: rsaPrivateKey2
|
|
1279
|
+
})), abortReason);
|
|
1280
|
+
assertEquals(requestCount, 1);
|
|
1281
|
+
} finally {
|
|
1282
|
+
esm_default.hardReset();
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
test("doubleKnock() prefers Request aborts over transport errors", async () => {
|
|
1286
|
+
esm_default.spyGlobal();
|
|
1287
|
+
try {
|
|
1288
|
+
let requestCount = 0;
|
|
1289
|
+
const controller = new AbortController();
|
|
1290
|
+
const abortReason = "transport aborted";
|
|
1291
|
+
esm_default.get("https://example.com/abort-with-transport-error", () => {
|
|
1292
|
+
requestCount++;
|
|
1293
|
+
controller.abort(abortReason);
|
|
1294
|
+
throw new TypeError("temporary DNS failure");
|
|
1295
|
+
});
|
|
1296
|
+
const request = new Request("https://example.com/abort-with-transport-error", { signal: controller.signal });
|
|
1297
|
+
assertEquals(await assertRejects(() => doubleKnock(request, {
|
|
1298
|
+
keyId: rsaPublicKey2.id,
|
|
1299
|
+
privateKey: rsaPrivateKey2
|
|
1300
|
+
})), abortReason);
|
|
1301
|
+
assertEquals(requestCount, 1);
|
|
1302
|
+
} finally {
|
|
1303
|
+
esm_default.hardReset();
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
982
1306
|
test("doubleKnock() async specDeterminer test", async () => {
|
|
983
1307
|
esm_default.spyGlobal();
|
|
984
1308
|
let requestCount = 0;
|
|
@@ -990,15 +1314,6 @@ test("doubleKnock() async specDeterminer test", async () => {
|
|
|
990
1314
|
else if (req.headers.has("Signature")) specUsed = "draft-cavage-http-signatures-12";
|
|
991
1315
|
return new Response("", { status: 202 });
|
|
992
1316
|
});
|
|
993
|
-
const specDeterminer = {
|
|
994
|
-
async determineSpec(_origin) {
|
|
995
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
996
|
-
return "draft-cavage-http-signatures-12";
|
|
997
|
-
},
|
|
998
|
-
async rememberSpec(_origin, _spec) {
|
|
999
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1000
|
-
}
|
|
1001
|
-
};
|
|
1002
1317
|
assertEquals((await doubleKnock(new Request("https://example.com/inbox-async-determiner", {
|
|
1003
1318
|
method: "POST",
|
|
1004
1319
|
body: "Test message with async spec determiner",
|
|
@@ -1006,7 +1321,15 @@ test("doubleKnock() async specDeterminer test", async () => {
|
|
|
1006
1321
|
}), {
|
|
1007
1322
|
keyId: rsaPublicKey2.id,
|
|
1008
1323
|
privateKey: rsaPrivateKey2
|
|
1009
|
-
}, { specDeterminer
|
|
1324
|
+
}, { specDeterminer: {
|
|
1325
|
+
async determineSpec(_origin) {
|
|
1326
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1327
|
+
return "draft-cavage-http-signatures-12";
|
|
1328
|
+
},
|
|
1329
|
+
async rememberSpec(_origin, _spec) {
|
|
1330
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1331
|
+
}
|
|
1332
|
+
} })).status, 202, "Response status should be 202 Accepted");
|
|
1010
1333
|
assertEquals(requestCount, 1, "Only one request should have been made");
|
|
1011
1334
|
assertEquals(specUsed, "draft-cavage-http-signatures-12", "Should use spec from async determiner");
|
|
1012
1335
|
esm_default.hardReset();
|