@calimero-network/mero-js 25.2.0 → 25.3.1
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/admin-api/admin-client.d.ts +3 -1
- package/dist/admin-api/admin-client.js +4 -0
- package/dist/admin-api/admin-types.d.ts +6 -0
- package/dist/events/sse.js +20 -8
- package/dist/http-client/http-types.d.ts +1 -1
- package/dist/http-client/web-client.d.ts +7 -0
- package/dist/http-client/web-client.js +41 -25
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +295 -267
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +295 -267
- package/dist/index.mjs.map +4 -4
- package/dist/request/proof-signer.d.ts +1 -1
- package/dist/request/request-sig.d.ts +9 -9
- package/dist/request/request-sig.js +7 -8
- package/dist/request-proof/authorizer.d.ts +3 -10
- package/dist/request-proof/authorizer.js +5 -10
- package/dist/request-proof/request-proof.d.ts +3 -6
- package/dist/request-proof/request-proof.js +4 -20
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpClient, HTTPError } from '../http-client/index.js';
|
|
2
|
-
import type { HealthStatus, ReadinessStatus, AdminAuthStatus, InstallApplicationRequest, InstallApplicationResponseData, InstallDevApplicationRequest, UninstallApplicationResponseData, ListApplicationsResponseData, GetApplicationResponseData, ApplicationVersionEntry, GetApplicationAbiResponseData, GetLatestVersionResponseData, ListPackagesResponseData, ListVersionsResponseData, CreateContextRequest, CreateContextResponseData, DeleteContextResponseData, GetContextsResponseData, Context, GenerateContextIdentityResponseData, GetContextIdentitiesResponseData, JoinContextResponseData, JoinSubgroupInheritanceResponseData, ContextGroupResponseData, ContextStorageResponseData, InviteSpecializedNodeRequest, InviteSpecializedNodeResponseData, UpdateContextApplicationRequest, ResyncContextRequest, ResyncContextResponseData, ContextsWithExecutorsResponseData, UploadBlobRequest, UploadBlobResponseData, DeleteBlobResponseData, ListBlobsResponseData, GetBlobInfoResponseData, BlobReadOptions, CreateContextAliasRequest, CreateApplicationAliasRequest, CreateDeviceAliasRequest, CreateAliasResponseData, LookupAliasResponseData, DeleteAliasResponseData, ListAliasesResponseData, ListNamespacesResponseData, CreateNamespaceRequest, CreateNamespaceResponseData, DeleteNamespaceResponseData, CreateNamespaceInvitationRequest, CreateNamespaceInvitationResponseData, CreateRecursiveInvitationResponseData, JoinNamespaceRequest, JoinNamespaceResponseData, CreateGroupInNamespaceRequest, CreateGroupInNamespaceResponseData, SubgroupEntry, Namespace, NamespaceIdentity, NodeIdentity, AccountPairInitRequest, AccountPairInitResponseData, AccountPairCompleteRequest, AccountPairCompleteResponseData, AccountSignWithRootRequest, AccountSignWithRootResponseData, RelinkDeviceRequest, RelinkDeviceResponseData, RescopeDeviceRequest, RescopeDeviceResponseData, LabelDeviceRequest, LabelDeviceResponseData, AccountDeviceEntry, AccountApplicationEntry, RevokeAccountDeviceRequest, LinkAccountDeviceRequest, LinkAccountDeviceResponseData, RevokeAccountDeviceResponseData, MemberDevicesEntry, ListMemberDevicesOptions, GroupInfoResponseData, CreateGroupRequest, CreateGroupResponseData, DeleteGroupResponseData, ListGroupMembersResponseData, ListGroupContextsResponseData, AddGroupMembersRequest, RemoveGroupMembersRequest, UpdateMemberRoleRequest, MemberCapabilities, SetMemberCapabilitiesRequest, SetDefaultCapabilitiesRequest, SetMemberAutoFollowRequest, SetSubgroupVisibilityRequest, SetTeeAdmissionPolicyRequest, GetTeeAdmissionPolicyResponseData, SetTeeAuthoringPolicyRequest, TransferOwnershipRequest, ChangeNamespaceAdminRequest, OwnerDeleteGroupRequest, RootGuardedOpRequest, SetGroupMetadataRequest, SetMemberMetadataRequest, SetContextMetadataRequest, MetadataRecord, SyncGroupResponseData, UpgradeGroupRequest, UpgradeGroupResponseData, GroupUpgradeStatusResponseData, MigrationStatus, CascadeStatusEntry, RetryGroupUpgradeResponseData, ReparentGroupRequest, ReparentGroupResponseData, IssueOwnershipProofRequest, IssueNamespaceOwnershipProofRequest, IssueOwnershipProofResponseData, CreateGroupInvitationRequest, CreateGroupInvitationResponseData, CreateRecursiveGroupInvitationResponseData, JoinGroupRequest, JoinGroupResponseData, TeeInfoResponseData, TeeAttestRequest, TeeAttestResponseData, TeeVerifyQuoteRequest, TeeVerifyQuoteResponseData, PerformIntentRequest, PerformIntentResponseData, IntentRelayInfo, QueryContextRequest, QueryContextResponseData, ListMemberDevicesResponseData, SealToAccountRequest, SealedEnvelope, AdmitJoinRequest, AdmitJoinResponseData, WarrantNonceState } from './admin-types.js';
|
|
2
|
+
import type { HealthStatus, ReadinessStatus, AdminAuthStatus, InstallApplicationRequest, InstallApplicationResponseData, InstallDevApplicationRequest, UninstallApplicationResponseData, ListApplicationsResponseData, GetApplicationResponseData, ApplicationVersionEntry, GetApplicationAbiResponseData, GetLatestVersionResponseData, ListPackagesResponseData, ListVersionsResponseData, CreateContextRequest, CreateContextResponseData, DeleteContextResponseData, GetContextsResponseData, Context, GenerateContextIdentityResponseData, GetContextIdentitiesResponseData, JoinContextResponseData, JoinSubgroupInheritanceResponseData, ContextGroupResponseData, ContextStorageResponseData, InviteSpecializedNodeRequest, InviteSpecializedNodeResponseData, UpdateContextApplicationRequest, ResyncContextRequest, ResyncContextResponseData, ContextsWithExecutorsResponseData, UploadBlobRequest, UploadBlobResponseData, DeleteBlobResponseData, ListBlobsResponseData, GetBlobInfoResponseData, BlobReadOptions, CreateContextAliasRequest, CreateApplicationAliasRequest, CreateDeviceAliasRequest, CreateAliasResponseData, LookupAliasResponseData, DeleteAliasResponseData, ListAliasesResponseData, ListNamespacesResponseData, CreateNamespaceRequest, CreateNamespaceResponseData, DeleteNamespaceResponseData, CreateNamespaceInvitationRequest, CreateNamespaceInvitationResponseData, CreateRecursiveInvitationResponseData, JoinNamespaceRequest, JoinNamespaceResponseData, CreateGroupInNamespaceRequest, CreateGroupInNamespaceResponseData, SubgroupEntry, Namespace, NamespaceIdentity, NodeIdentity, AccountPairInitRequest, AccountPairInitResponseData, AccountPairCompleteRequest, AccountPairCompleteResponseData, AccountSignWithRootRequest, AccountSignWithRootResponseData, RelinkDeviceRequest, RelinkDeviceResponseData, RescopeDeviceRequest, RescopeDeviceResponseData, LabelDeviceRequest, LabelDeviceResponseData, AccountDeviceEntry, AccountApplicationEntry, RevokeAccountDeviceRequest, LinkAccountDeviceRequest, LinkAccountDeviceResponseData, RevokeAccountDeviceResponseData, MemberDevicesEntry, ListMemberDevicesOptions, GroupInfoResponseData, CreateGroupRequest, CreateGroupResponseData, DeleteGroupResponseData, ListGroupMembersResponseData, ListGroupContextsResponseData, AddGroupMembersRequest, RemoveGroupMembersRequest, UpdateMemberRoleRequest, MemberCapabilities, SetMemberCapabilitiesRequest, SetDefaultCapabilitiesRequest, SetMemberAutoFollowRequest, SetSubgroupVisibilityRequest, SetTeeAdmissionPolicyRequest, GetTeeAdmissionPolicyResponseData, SetTeeAuthoringPolicyRequest, TransferOwnershipRequest, ChangeNamespaceAdminRequest, OwnerDeleteGroupRequest, RootGuardedOpRequest, SetGroupMetadataRequest, SetMemberMetadataRequest, SetContextMetadataRequest, MetadataRecord, SyncGroupResponseData, UpgradeGroupRequest, UpgradeGroupResponseData, GroupUpgradeStatusResponseData, MigrationStatus, NamespaceRevokedDevices, CascadeStatusEntry, RetryGroupUpgradeResponseData, ReparentGroupRequest, ReparentGroupResponseData, IssueOwnershipProofRequest, IssueNamespaceOwnershipProofRequest, IssueOwnershipProofResponseData, CreateGroupInvitationRequest, CreateGroupInvitationResponseData, CreateRecursiveGroupInvitationResponseData, JoinGroupRequest, JoinGroupResponseData, TeeInfoResponseData, TeeAttestRequest, TeeAttestResponseData, TeeVerifyQuoteRequest, TeeVerifyQuoteResponseData, PerformIntentRequest, PerformIntentResponseData, IntentRelayInfo, QueryContextRequest, QueryContextResponseData, ListMemberDevicesResponseData, SealToAccountRequest, SealedEnvelope, AdmitJoinRequest, AdmitJoinResponseData, WarrantNonceState } from './admin-types.js';
|
|
3
3
|
import { type RedeemOutcome } from './redeem-invitation.js';
|
|
4
4
|
/**
|
|
5
5
|
* Thrown when a node answers the warrant-nonce route with a 404.
|
|
@@ -720,6 +720,8 @@ export declare class AdminApiClient {
|
|
|
720
720
|
* envelope to unwrap here (unlike most admin reads).
|
|
721
721
|
*/
|
|
722
722
|
getMigrationStatus(namespaceId: string): Promise<MigrationStatus>;
|
|
723
|
+
/** Revoked devices and withdrawn slots of a namespace; 403 unless this node is a bound member. */
|
|
724
|
+
listNamespaceRevokedDevices(namespaceId: string): Promise<NamespaceRevokedDevices>;
|
|
723
725
|
/** Per-group cascade-migration snapshots for a namespace. */
|
|
724
726
|
getCascadeStatus(namespaceId: string): Promise<CascadeStatusEntry[]>;
|
|
725
727
|
retryGroupUpgrade(groupId: string): Promise<RetryGroupUpgradeResponseData>;
|
|
@@ -1285,6 +1285,10 @@ export class AdminApiClient {
|
|
|
1285
1285
|
async getMigrationStatus(namespaceId) {
|
|
1286
1286
|
return this.httpClient.get(`/admin-api/groups/${pathSegment(namespaceId)}/migration-status`);
|
|
1287
1287
|
}
|
|
1288
|
+
/** Revoked devices and withdrawn slots of a namespace; 403 unless this node is a bound member. */
|
|
1289
|
+
async listNamespaceRevokedDevices(namespaceId) {
|
|
1290
|
+
return this.httpClient.get(`/admin-api/namespaces/${pathSegment(namespaceId)}/revoked-devices`);
|
|
1291
|
+
}
|
|
1288
1292
|
/** Per-group cascade-migration snapshots for a namespace. */
|
|
1289
1293
|
async getCascadeStatus(namespaceId) {
|
|
1290
1294
|
return unwrap(await this.httpClient.get(`/admin-api/groups/${pathSegment(namespaceId)}/cascade-status`));
|
|
@@ -1353,6 +1353,12 @@ export interface MigrationStatus {
|
|
|
1353
1353
|
rollup: MigrationStatusRollup;
|
|
1354
1354
|
members: MemberMigrationStatusEntry[];
|
|
1355
1355
|
}
|
|
1356
|
+
export interface NamespaceRevokedDevices {
|
|
1357
|
+
/** Device ids (64 hex) revoked in the namespace. */
|
|
1358
|
+
revokedDevices: string[];
|
|
1359
|
+
/** Hex `sha256(account || device)` of each device its own account withdrew in the namespace. */
|
|
1360
|
+
withdrawnSlots: string[];
|
|
1361
|
+
}
|
|
1356
1362
|
export interface CascadeStatusEntry {
|
|
1357
1363
|
groupId: string;
|
|
1358
1364
|
upgrade: GroupUpgradeStatus;
|
package/dist/events/sse.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isGroupMigrationEvent } from './group.js';
|
|
2
2
|
import { AuthRevokedError, HTTPError } from '../http-client/index.js';
|
|
3
|
+
import { carriesProof, refuseProofRedirect } from '../http-client/web-client.js';
|
|
4
|
+
import { signedPath } from '../request-proof/authorizer.js';
|
|
3
5
|
/**
|
|
4
6
|
* `x-auth-error` reasons that mean the whole token family is gone, mirroring
|
|
5
7
|
* the request path in `web-client.ts`. Anything else — `token_expired` above
|
|
@@ -143,13 +145,18 @@ export class SseClient {
|
|
|
143
145
|
this.abortController = new AbortController();
|
|
144
146
|
try {
|
|
145
147
|
const url = `${this.baseUrl}/sse`;
|
|
148
|
+
const headers = {
|
|
149
|
+
...(await this.authorize({ method: 'GET', path: signedPath(url) })),
|
|
150
|
+
'Accept': 'text/event-stream',
|
|
151
|
+
};
|
|
152
|
+
const proofAttached = carriesProof(headers);
|
|
146
153
|
const response = await this.fetchImpl(url, {
|
|
147
|
-
headers
|
|
148
|
-
...(await this.authorize({ method: 'GET', path: new URL(url).pathname })),
|
|
149
|
-
'Accept': 'text/event-stream',
|
|
150
|
-
},
|
|
154
|
+
headers,
|
|
151
155
|
signal: this.abortController.signal,
|
|
156
|
+
...(proofAttached && { redirect: 'manual' }),
|
|
152
157
|
});
|
|
158
|
+
if (proofAttached)
|
|
159
|
+
refuseProofRedirect(response, url);
|
|
153
160
|
if (!response.ok) {
|
|
154
161
|
// Throw the same typed errors the request path throws. A bare
|
|
155
162
|
// `Error("SSE connection failed: 401")` forced callers to string-match
|
|
@@ -379,15 +386,20 @@ export class SseClient {
|
|
|
379
386
|
// differ in key order, which fails as a refused credential rather than as
|
|
380
387
|
// anything that names the real cause.
|
|
381
388
|
const body = JSON.stringify({ id: this.sessionId, method, params });
|
|
389
|
+
const headers = {
|
|
390
|
+
...(await this.authorize({ method: 'POST', path: signedPath(url), body })),
|
|
391
|
+
'Content-Type': 'application/json',
|
|
392
|
+
};
|
|
393
|
+
const proofAttached = carriesProof(headers);
|
|
382
394
|
const response = await this.fetchImpl(url, {
|
|
383
395
|
method: 'POST',
|
|
384
|
-
headers
|
|
385
|
-
...(await this.authorize({ method: 'POST', path: new URL(url).pathname, body })),
|
|
386
|
-
'Content-Type': 'application/json',
|
|
387
|
-
},
|
|
396
|
+
headers,
|
|
388
397
|
body,
|
|
389
398
|
signal: timeout.signal,
|
|
399
|
+
...(proofAttached && { redirect: 'manual' }),
|
|
390
400
|
});
|
|
401
|
+
if (proofAttached)
|
|
402
|
+
refuseProofRedirect(response, url);
|
|
391
403
|
if (!response.ok) {
|
|
392
404
|
// The subscribe path used to report only the status. On the one failure
|
|
393
405
|
// that matters it is the whole of what the caller got: a revoked family
|
|
@@ -18,7 +18,7 @@ export interface Transport {
|
|
|
18
18
|
*/
|
|
19
19
|
authorizeRequest?: (request: {
|
|
20
20
|
method: string;
|
|
21
|
-
/** The path the node will see,
|
|
21
|
+
/** The path and query the node will see, as `signedPath` reads them. */
|
|
22
22
|
path: string;
|
|
23
23
|
body?: string | Uint8Array;
|
|
24
24
|
}) => Promise<Record<string, string>>;
|
|
@@ -36,6 +36,13 @@ export declare class AuthRevokedError extends HTTPError {
|
|
|
36
36
|
/** Value of the `x-auth-error` header, e.g. `token_reuse` or `token_revoked`. */
|
|
37
37
|
reason: string, status: number, statusText: string, url: string, headers: Headers, bodyText?: string);
|
|
38
38
|
}
|
|
39
|
+
/** Whether `headers` carry a request proof, which must never follow a redirect. */
|
|
40
|
+
export declare function carriesProof(headers: Record<string, string>): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Throws when a proof-carrying request was answered with a redirect, which
|
|
43
|
+
* fetch reports as an opaque response with status 0 under redirect: 'manual'.
|
|
44
|
+
*/
|
|
45
|
+
export declare function refuseProofRedirect(response: Response, url: string): void;
|
|
39
46
|
export declare class WebHttpClient implements HttpClient {
|
|
40
47
|
private transport;
|
|
41
48
|
private refreshTokenPromise;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { combineSignals, createTimeoutSignal } from './signal-utils.js';
|
|
2
|
+
import { signedPath } from '../request-proof/authorizer.js';
|
|
3
|
+
import { REQUEST_PROOF_HEADER } from '../request-proof/request-proof.js';
|
|
2
4
|
/**
|
|
3
5
|
* `x-auth-error` reasons that mean the whole token family is gone.
|
|
4
6
|
*
|
|
@@ -93,24 +95,36 @@ function headersToRecord(headers) {
|
|
|
93
95
|
});
|
|
94
96
|
return record;
|
|
95
97
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* `new URL` drops the query and keeps any prefix the base URL contributed,
|
|
101
|
-
* which is exactly what reaches the server. The fallback covers a `url` that is
|
|
102
|
-
* already relative — it cannot be parsed, and signing the whole string with its
|
|
103
|
-
* query attached would be worse than signing the part before the `?`.
|
|
104
|
-
*/
|
|
105
|
-
function pathForProof(url) {
|
|
98
|
+
/** Runtimes disagree on sending a bare `?`, so the URL parser drops it once and
|
|
99
|
+
* the URL signed and the URL fetched are one string. */
|
|
100
|
+
function withoutBareQuery(url) {
|
|
101
|
+
let parsed;
|
|
106
102
|
try {
|
|
107
|
-
|
|
103
|
+
parsed = new URL(url, globalThis.location?.href);
|
|
108
104
|
}
|
|
109
105
|
catch {
|
|
110
|
-
|
|
111
|
-
return query === -1 ? url : url.slice(0, query);
|
|
106
|
+
return url; // relative, with no page to resolve it against
|
|
112
107
|
}
|
|
108
|
+
if (parsed.search === '')
|
|
109
|
+
parsed.search = '';
|
|
110
|
+
return parsed.href;
|
|
111
|
+
}
|
|
112
|
+
/** Whether `headers` carry a request proof, which must never follow a redirect. */
|
|
113
|
+
export function carriesProof(headers) {
|
|
114
|
+
return Object.keys(headers).some((name) => name.toLowerCase() === REQUEST_PROOF_HEADER.toLowerCase());
|
|
113
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Throws when a proof-carrying request was answered with a redirect, which
|
|
118
|
+
* fetch reports as an opaque response with status 0 under redirect: 'manual'.
|
|
119
|
+
*/
|
|
120
|
+
export function refuseProofRedirect(response, url) {
|
|
121
|
+
const redirected = response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400);
|
|
122
|
+
if (!redirected)
|
|
123
|
+
return;
|
|
124
|
+
// Status 0 marks a withheld response; its explanation is the body text.
|
|
125
|
+
throw new HTTPError(0, 'Redirect Not Followed', url, response.headers, `the node answered with a redirect; a request carrying ${REQUEST_PROOF_HEADER} is not followed to another location`);
|
|
126
|
+
}
|
|
127
|
+
// Web Standards HTTP client implementation
|
|
114
128
|
export class WebHttpClient {
|
|
115
129
|
constructor(transport) {
|
|
116
130
|
this.transport = transport;
|
|
@@ -175,7 +189,7 @@ export class WebHttpClient {
|
|
|
175
189
|
async makeRequest(path, init, retryCount = 0, requestStartTime) {
|
|
176
190
|
// Maximum retry attempts to prevent infinite loops
|
|
177
191
|
const MAX_RETRY_ATTEMPTS = 1;
|
|
178
|
-
const url = this.buildUrl(path);
|
|
192
|
+
const url = withoutBareQuery(this.buildUrl(path));
|
|
179
193
|
// Track request start time for timeout calculation (only on first attempt)
|
|
180
194
|
// Use per-request start time to avoid corruption from concurrent requests
|
|
181
195
|
const startTime = requestStartTime ?? Date.now();
|
|
@@ -195,9 +209,7 @@ export class WebHttpClient {
|
|
|
195
209
|
}
|
|
196
210
|
const method = init?.method || 'GET';
|
|
197
211
|
// A credential that commits to the request can only be produced here, where
|
|
198
|
-
// the method, the assembled path and the body are all known.
|
|
199
|
-
// from the URL rather than from `path`, because a base URL may carry a prefix
|
|
200
|
-
// (`…/admin-api`) that the node sees and a bare relative path would not sign.
|
|
212
|
+
// the method, the assembled path and query, and the body are all known.
|
|
201
213
|
if (this.transport.authorizeRequest) {
|
|
202
214
|
const body = init?.body;
|
|
203
215
|
if (body !== undefined && typeof body !== 'string' && !(body instanceof Uint8Array)) {
|
|
@@ -207,7 +219,7 @@ export class WebHttpClient {
|
|
|
207
219
|
}
|
|
208
220
|
Object.assign(headersObj, await this.transport.authorizeRequest({
|
|
209
221
|
method,
|
|
210
|
-
path:
|
|
222
|
+
path: signedPath(url),
|
|
211
223
|
// Narrowed by the guard above; `BodyInit` is wider than what can be
|
|
212
224
|
// signed.
|
|
213
225
|
body: body,
|
|
@@ -281,24 +293,28 @@ export class WebHttpClient {
|
|
|
281
293
|
// nor the body — and a proof commits to both. Signed AFTER the body is
|
|
282
294
|
// settled above, so what is hashed is what is sent.
|
|
283
295
|
//
|
|
284
|
-
// The path
|
|
285
|
-
//
|
|
286
|
-
// a base URL that contributes its own prefix. The node verifies against the
|
|
287
|
-
// path it received, so the only safe source is the URL actually being
|
|
288
|
-
// fetched. A query string is deliberately not covered — core's `RequestSig`
|
|
289
|
-
// signs the path alone, and signing more here would fail every request.
|
|
296
|
+
// The path and query come from the built URL, not from the `path` argument,
|
|
297
|
+
// which may be relative to a base URL that contributes its own prefix.
|
|
290
298
|
if (this.transport.getProof) {
|
|
291
299
|
const proof = await this.transport.getProof({
|
|
292
300
|
method: requestInit.method ?? 'GET',
|
|
293
|
-
path:
|
|
301
|
+
path: signedPath(url),
|
|
294
302
|
body: requestInit.body,
|
|
295
303
|
});
|
|
296
304
|
if (proof) {
|
|
297
305
|
headersObj['X-Calimero-Proof'] = proof;
|
|
298
306
|
}
|
|
299
307
|
}
|
|
308
|
+
// fetch forwards custom headers across redirects, so a proof must never be
|
|
309
|
+
// sent anywhere but where it was signed for.
|
|
310
|
+
const proofAttached = carriesProof(headersObj);
|
|
311
|
+
if (proofAttached && requestInit.redirect !== 'error') {
|
|
312
|
+
requestInit.redirect = 'manual';
|
|
313
|
+
}
|
|
300
314
|
try {
|
|
301
315
|
const response = await this.transport.fetch(url, requestInit);
|
|
316
|
+
if (proofAttached)
|
|
317
|
+
refuseProofRedirect(response, url);
|
|
302
318
|
if (!response.ok) {
|
|
303
319
|
const bodyText = await this.getBodyText(response);
|
|
304
320
|
const httpError = new HTTPError(response.status, response.statusText, url, response.headers, bodyText);
|