@calimero-network/mero-js 26.4.1 → 26.6.0

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.
@@ -4,8 +4,8 @@ export { resolveRoot } from './account.js';
4
4
  export type { RootSource } from './account.js';
5
5
  export { resolveRootPair } from './account.js';
6
6
  export type { ResolvedRoot } from './account.js';
7
- export type { DelegatedCredential, DelegatedSession as DelegatedAccountSession, RelayMap, RelayNodeKeyAttempt } from './session.js';
8
- export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, } from './session.js';
7
+ export type { DelegatedCredential, DelegatedSession as DelegatedAccountSession, RelayMap, RelayNodeKeyAttempt, RelayListingFailure } from './session.js';
8
+ export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, RelaysUnavailableError, } from './session.js';
9
9
  export { resolveRelayFromInvitation, normaliseAccount } from './relay-from-invitation.js';
10
10
  export type { RelayResolutionStep, RelayResolutionFailure, ResolvedInvitationRelay, ResolveRelayResult, ResolveRelayFromInvitationInput, } from './relay-from-invitation.js';
11
11
  export { joinWithNode } from './join-with-node.js';
@@ -20,5 +20,7 @@ export { applicationIdForBundle, fetchRegistryBundles, resolveApplicationIdFromR
20
20
  export type { RegistryBundle, ResolvedApplication } from './application-id.js';
21
21
  export { createAccountAdmin, InvitationNotClaimableError, NoRelayError, NotForAccountError, } from './account-admin.js';
22
22
  export type { AccountAdminDeps } from './account-admin.js';
23
+ export { RelayLoginError } from './relay-login-error.js';
24
+ export type { RelayLoginFailureKind } from './relay-login-error.js';
23
25
  export { createNodeAdmin } from './node-admin.js';
24
26
  export type { NodeAdminDeps } from './node-admin.js';
@@ -2,7 +2,7 @@
2
2
  export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, createAccountRootSigner, accountRootSignerFromPhrase, signAccountLink, signAccountLogin, } from './account.js';
3
3
  export { resolveRoot } from './account.js';
4
4
  export { resolveRootPair } from './account.js';
5
- export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, } from './session.js';
5
+ export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, RelaysUnavailableError, } from './session.js';
6
6
  export { resolveRelayFromInvitation, normaliseAccount } from './relay-from-invitation.js';
7
7
  export { joinWithNode } from './join-with-node.js';
8
8
  export { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
@@ -11,4 +11,5 @@ export { createDelegatedContext, createDelegatedPrivateContext, foundDelegatedNa
11
11
  export { governGroup, governRoot, namespaceOfGroup, rememberGroupNamespace } from './govern.js';
12
12
  export { applicationIdForBundle, fetchRegistryBundles, resolveApplicationIdFromRegistry, selectLatestBundle, } from './application-id.js';
13
13
  export { createAccountAdmin, InvitationNotClaimableError, NoRelayError, NotForAccountError, } from './account-admin.js';
14
+ export { RelayLoginError } from './relay-login-error.js';
14
15
  export { createNodeAdmin } from './node-admin.js';
@@ -0,0 +1,32 @@
1
+ import { RequestAuthError } from '../http-client/web-client.js';
2
+ /**
3
+ * Why a relay login failed, as far as it decides what the app does next.
4
+ *
5
+ * - `signed-out` — the relay answered and refused the credential (401 or 403):
6
+ * it expired or was revoked, and signing in again is the way forward.
7
+ * - `unavailable` — the login never got an answer about the credential: the
8
+ * relay could not be reached (DNS, TLS, offline, a timeout), or it failed
9
+ * with a 5xx, or answered something else. Trying again later may work.
10
+ */
11
+ export type RelayLoginFailureKind = 'signed-out' | 'unavailable';
12
+ /**
13
+ * The relay session a request needed could not be minted.
14
+ *
15
+ * Thrown by the admin requests of a delegated client (`client.admin.*`) in
16
+ * place of sending them without a token, so the caller sees the login's own
17
+ * failure rather than the node's 401 for the unauthenticated request — or, for
18
+ * a relay that is down, the same transport error a second time. Nothing is
19
+ * remembered: the next request logs in again.
20
+ */
21
+ export declare class RelayLoginError extends RequestAuthError {
22
+ name: string;
23
+ readonly relayUrl: string;
24
+ /** The login's own failure: an `HTTPError` (status 0 for no response) or what it threw. */
25
+ readonly cause: unknown;
26
+ readonly kind: RelayLoginFailureKind;
27
+ /** The HTTP status the login answered, 0 when no response arrived; undefined when not an HTTP failure. */
28
+ readonly loginStatus: number | undefined;
29
+ constructor(relayUrl: string, cause: unknown);
30
+ }
31
+ /** `signed-out` for a login answered 401/403, `unavailable` for anything else. */
32
+ export declare function relayLoginFailureKind(cause: unknown): RelayLoginFailureKind;
@@ -0,0 +1,34 @@
1
+ import { RequestAuthError } from '../http-client/web-client.js';
2
+ /**
3
+ * The relay session a request needed could not be minted.
4
+ *
5
+ * Thrown by the admin requests of a delegated client (`client.admin.*`) in
6
+ * place of sending them without a token, so the caller sees the login's own
7
+ * failure rather than the node's 401 for the unauthenticated request — or, for
8
+ * a relay that is down, the same transport error a second time. Nothing is
9
+ * remembered: the next request logs in again.
10
+ */
11
+ export class RelayLoginError extends RequestAuthError {
12
+ constructor(relayUrl, cause) {
13
+ const kind = relayLoginFailureKind(cause);
14
+ const detail = cause instanceof Error ? cause.message : String(cause);
15
+ super(kind === 'signed-out'
16
+ ? `the relay ${relayUrl} refused this device's credential (${detail}); sign in again`
17
+ : `could not log in to the relay ${relayUrl}: ${detail}`);
18
+ this.name = 'RelayLoginError';
19
+ this.cause = cause;
20
+ this.relayUrl = relayUrl;
21
+ this.kind = kind;
22
+ this.loginStatus = loginStatusOf(cause);
23
+ }
24
+ }
25
+ /** `signed-out` for a login answered 401/403, `unavailable` for anything else. */
26
+ export function relayLoginFailureKind(cause) {
27
+ const status = loginStatusOf(cause);
28
+ return status === 401 || status === 403 ? 'signed-out' : 'unavailable';
29
+ }
30
+ /** The status a login failure carries (`HTTPError.status`), if any. */
31
+ function loginStatusOf(cause) {
32
+ const status = cause?.status;
33
+ return typeof status === 'number' && Number.isFinite(status) ? status : undefined;
34
+ }
@@ -209,6 +209,36 @@ export declare function relayForContext(account: string, contextId: string): str
209
209
  export declare function carryExecutorAccount(prev: DelegatedSession | null, next: DelegatedSession): DelegatedSession;
210
210
  /** Every relay this account is known to use: the session's, and each in the map. */
211
211
  export declare function knownRelays(s: DelegatedSession): string[];
212
+ /**
213
+ * Thrown by {@link listDelegatedContexts} and {@link listDelegatedNamespaces}
214
+ * when the account knows relays and **none** of them answered.
215
+ *
216
+ * One relay down is skipped, as before: the others still answer for what they
217
+ * serve. Every relay down is different. Returning `[]` then reads as "a member
218
+ * of nothing", and an app renders an empty account, with no error, for what is
219
+ * really "could not ask". A relay whose hostname no longer resolves (a node
220
+ * rebuilt or deleted in the cloud) did exactly that.
221
+ */
222
+ /**
223
+ * Why one relay did not answer a listing.
224
+ *
225
+ * `refused`: its attestation did not verify (an untrusted image, a release
226
+ * below the floor). Asking again cannot change that, and it may be worth a
227
+ * closer look. `unavailable`: something on the way did not answer, or the
228
+ * relay refused the request itself; trying again later may.
229
+ */
230
+ export interface RelayListingFailure {
231
+ relayUrl: string;
232
+ kind: 'refused' | 'unavailable';
233
+ error: unknown;
234
+ }
235
+ export declare class RelaysUnavailableError extends Error {
236
+ readonly failures: readonly RelayListingFailure[];
237
+ readonly relays: readonly string[];
238
+ constructor(failures: readonly RelayListingFailure[]);
239
+ /** Every relay refused its attestation: retrying will not help. */
240
+ get refused(): boolean;
241
+ }
212
242
  type ListedContext = Awaited<ReturnType<MeroClient['admin']['getContexts']>>['contexts'][number];
213
243
  /**
214
244
  * This account's contexts on every relay it knows, each tagged with the relay
@@ -216,7 +246,8 @@ type ListedContext = Awaited<ReturnType<MeroClient['admin']['getContexts']>>['co
216
246
  *
217
247
  * Each relay answers only for itself and only for this caller, so the union is
218
248
  * exactly the account's contexts across relays. A relay that cannot be reached
219
- * or logged in to is skipped (its contexts simply do not appear), not fatal.
249
+ * or logged in to is skipped (its contexts simply do not appear), not fatal,
250
+ * unless it is every relay: then {@link RelaysUnavailableError}.
220
251
  */
221
252
  export declare function listDelegatedContexts(s: DelegatedSession): Promise<Array<ListedContext & {
222
253
  relayUrl: string;
@@ -226,7 +257,8 @@ type ListedNamespace = Awaited<ReturnType<MeroClient['admin']['listNamespaces']>
226
257
  * This account's namespaces on every relay it knows, each tagged with the relay
227
258
  * that listed it and recorded as that namespace's relay. Same rules as
228
259
  * {@link listDelegatedContexts}: each relay answers for itself and this caller
229
- * only (`namespace:list-own`); an unreachable relay is skipped.
260
+ * only (`namespace:list-own`); an unreachable relay is skipped, and every
261
+ * relay unreachable is {@link RelaysUnavailableError}.
230
262
  */
231
263
  export declare function listDelegatedNamespaces(s: DelegatedSession): Promise<Array<ListedNamespace & {
232
264
  relayUrl: string;
@@ -17,6 +17,7 @@ import { attestRelayNodeKey, } from '../relay-attestation/index.js';
17
17
  import { cloudNodeReleaseUrl, createAttestedSealedFetch, createSignedReleaseVerifier, DEFAULT_RELEASE_MIRROR, fetchNodeRelease, fetchNodeReleaseVersion, } from '../sealed/index.js';
18
18
  import { createMeroClient, defaultAudience, } from '../transport/index.js';
19
19
  import { login, } from '../login/index.js';
20
+ import { RelayLoginError } from './relay-login-error.js';
20
21
  import { SseClient, } from '../events/index.js';
21
22
  /** Where the per-tab record lives. */
22
23
  const KEY = 'calimero.delegated.connection';
@@ -517,9 +518,12 @@ export function relayTransportFetch(relayUrl) {
517
518
  * A hosted relay's `/admin-api/` sits behind the node's forward-auth, which
518
519
  * knows no request proof — it answers a Bearer token or 401. Logging in with
519
520
  * the device certificate is how a keyholder gets that token. Minted on first
520
- * use and re-minted once it has expired. On failure it yields `undefined`, the
521
- * admin request then goes out unauthenticated and the node's 401 is what the
522
- * caller sees; the next call tries to log in again.
521
+ * use and re-minted once it has expired. On failure it throws a
522
+ * {@link RelayLoginError}, which the HTTP client lets through instead of sending
523
+ * the admin request unauthenticated: the caller sees why the login failed —
524
+ * `signed-out` for a refused credential, `unavailable` for a relay that did not
525
+ * answer — rather than the node's 401 or the same transport error twice.
526
+ * Nothing is cached on failure, so the next call tries to log in again.
523
527
  */
524
528
  function relaySession(s, nodeKey) {
525
529
  let token = null;
@@ -543,9 +547,8 @@ function relaySession(s, nodeKey) {
543
547
  return token;
544
548
  })
545
549
  .catch((e) => {
546
- console.warn('[mero-js] relay login failed', e);
547
550
  token = null;
548
- return undefined;
551
+ throw new RelayLoginError(s.relayUrl, e);
549
552
  })
550
553
  .finally(() => {
551
554
  inflight = null;
@@ -804,24 +807,56 @@ export function knownRelays(s) {
804
807
  .map(relayOrigin);
805
808
  return [...new Set(all)];
806
809
  }
810
+ export class RelaysUnavailableError extends Error {
811
+ constructor(failures) {
812
+ super(`none of this account's relays answered: ${failures.map((f) => `${f.relayUrl} (${f.kind})`).join(', ')}`);
813
+ this.failures = failures;
814
+ this.name = 'RelaysUnavailableError';
815
+ this.relays = failures.map((f) => f.relayUrl);
816
+ }
817
+ /** Every relay refused its attestation: retrying will not help. */
818
+ get refused() {
819
+ return this.failures.every((f) => f.kind === 'refused');
820
+ }
821
+ }
822
+ /**
823
+ * One relay's client for a listing, or the failure that stopped it. The key's
824
+ * own outcome is kept, so a refused attestation is not reported as a relay
825
+ * that merely did not answer.
826
+ */
827
+ async function listingClient(s, relayUrl) {
828
+ const key = await attemptRelayNodeKey(relayUrl);
829
+ if (key.kind !== 'learned') {
830
+ console.warn('[mero-js] could not learn the relay node key from its attestation', key.error);
831
+ return { failure: { relayUrl, kind: key.kind, error: key.error } };
832
+ }
833
+ const client = buildDelegatedClient({ ...s, relayUrl }, null);
834
+ if (!client)
835
+ return { failure: { relayUrl, kind: 'unavailable', error: new Error(`no client for ${relayUrl}`) } };
836
+ return { client };
837
+ }
807
838
  /**
808
839
  * This account's contexts on every relay it knows, each tagged with the relay
809
840
  * that listed it — and that relay recorded as the one serving it.
810
841
  *
811
842
  * Each relay answers only for itself and only for this caller, so the union is
812
843
  * exactly the account's contexts across relays. A relay that cannot be reached
813
- * or logged in to is skipped (its contexts simply do not appear), not fatal.
844
+ * or logged in to is skipped (its contexts simply do not appear), not fatal,
845
+ * unless it is every relay: then {@link RelaysUnavailableError}.
814
846
  */
815
847
  export async function listDelegatedContexts(s) {
816
848
  const out = [];
817
849
  const map = readRelayMap(s.account);
818
- for (const relayUrl of knownRelays(s)) {
850
+ const relays = knownRelays(s);
851
+ const failures = [];
852
+ for (const relayUrl of relays) {
853
+ const got = await listingClient(s, relayUrl);
854
+ if ('failure' in got) {
855
+ failures.push(got.failure);
856
+ continue;
857
+ }
858
+ const { client } = got;
819
859
  try {
820
- if (!(await resolveRelayNodeKey(relayUrl)))
821
- continue;
822
- const client = buildDelegatedClient({ ...s, relayUrl }, null);
823
- if (!client)
824
- continue;
825
860
  const { contexts } = await client.admin.getContexts();
826
861
  for (const c of contexts ?? []) {
827
862
  out.push({ ...c, relayUrl });
@@ -829,30 +864,37 @@ export async function listDelegatedContexts(s) {
829
864
  }
830
865
  }
831
866
  catch (e) {
867
+ failures.push({ relayUrl, kind: 'unavailable', error: e });
832
868
  console.warn(`[mero-js] could not list contexts on ${relayUrl}`, e);
833
869
  }
834
870
  }
835
871
  writeRelayMap(s.account, map);
872
+ if (relays.length > 0 && failures.length === relays.length)
873
+ throw new RelaysUnavailableError(failures);
836
874
  return out;
837
875
  }
838
876
  /**
839
877
  * This account's namespaces on every relay it knows, each tagged with the relay
840
878
  * that listed it and recorded as that namespace's relay. Same rules as
841
879
  * {@link listDelegatedContexts}: each relay answers for itself and this caller
842
- * only (`namespace:list-own`); an unreachable relay is skipped.
880
+ * only (`namespace:list-own`); an unreachable relay is skipped, and every
881
+ * relay unreachable is {@link RelaysUnavailableError}.
843
882
  */
844
883
  export async function listDelegatedNamespaces(s) {
845
884
  var _a, _b;
846
885
  const out = [];
847
886
  const seen = new Set();
848
887
  const map = readRelayMap(s.account);
849
- for (const relayUrl of knownRelays(s)) {
888
+ const relays = knownRelays(s);
889
+ const failures = [];
890
+ for (const relayUrl of relays) {
891
+ const got = await listingClient(s, relayUrl);
892
+ if ('failure' in got) {
893
+ failures.push(got.failure);
894
+ continue;
895
+ }
896
+ const { client } = got;
850
897
  try {
851
- if (!(await resolveRelayNodeKey(relayUrl)))
852
- continue;
853
- const client = buildDelegatedClient({ ...s, relayUrl }, null);
854
- if (!client)
855
- continue;
856
898
  for (const ns of await client.admin.listNamespaces()) {
857
899
  if (seen.has(ns.namespaceId))
858
900
  continue;
@@ -862,9 +904,12 @@ export async function listDelegatedNamespaces(s) {
862
904
  }
863
905
  }
864
906
  catch (e) {
907
+ failures.push({ relayUrl, kind: 'unavailable', error: e });
865
908
  console.warn(`[mero-js] could not list namespaces on ${relayUrl}`, e);
866
909
  }
867
910
  }
868
911
  writeRelayMap(s.account, map);
912
+ if (relays.length > 0 && failures.length === relays.length)
913
+ throw new RelaysUnavailableError(failures);
869
914
  return out;
870
915
  }
@@ -16,6 +16,7 @@
16
16
  // the request resolving, and the namespace being listed. Either one is enough,
17
17
  // which matters because either one can be missing.
18
18
  import { classifyError, messageOf } from '../errors/classify.js';
19
+ import { RelayLoginError } from '../account/relay-login-error.js';
19
20
  /** Whether an outcome means the invitation is finished with. */
20
21
  export function isSettled(outcome) {
21
22
  return outcome.status !== 'failed' || !outcome.retryable;
@@ -111,6 +112,11 @@ const FINAL_FROM_TEXT = new Set(['already-used', 'removed']);
111
112
  * invitation, and a 404 is not specific enough to throw a link away on.
112
113
  */
113
114
  function classifyJoinFailure(err, message) {
115
+ // The join never left: the relay session it needed could not be minted. Says
116
+ // nothing about the invitation, which is kept for after a sign-in or a retry.
117
+ if (err instanceof RelayLoginError) {
118
+ return { reason: err.kind === 'signed-out' ? 'signed-out' : 'node-unreachable', retryable: true };
119
+ }
114
120
  const { kind, status } = classifyError(err);
115
121
  const fromText = reasonFromMessage(message);
116
122
  // A code is the node naming the refusal itself: nothing below can know better.
@@ -1,16 +1,4 @@
1
- // ── What a failed request means, in terms a caller can act on ────────────────
2
- //
3
- // Core rc.56 onwards (calimero-network/core#4151 to #4164) answers a refused
4
- // admin request with the status that names the refusal: a malformed body is a
5
- // 400, a caller the node will not serve a 403, a missing resource a 404, a
6
- // state the request contradicts (an expired invitation, a removed member) a
7
- // 409, an oversized upload a 413, and a request the node cannot serve *yet*
8
- // (no member online to hand over a group key) a 503. Before that, every one of
9
- // them was a bare 500 and the only way to tell them apart was to match text.
10
- //
11
- // Every caller asks the same two questions of such an error: what kind of
12
- // failure is this, and would doing it again later help? This answers both once,
13
- // so apps stop keeping their own copies of the status table.
1
+ import { RelayLoginError } from '../account/relay-login-error.js';
14
2
  const RETRYABLE = {
15
3
  invalid: false,
16
4
  // The session, not the request: signing in again and repeating it works.
@@ -106,6 +94,14 @@ export function messageOf(err, fallback = 'Request failed') {
106
94
  * own status-0 `HTTPError`.
107
95
  */
108
96
  export function classifyError(err) {
97
+ // A relay login that failed before the request was sent: its own status is
98
+ // about the login, so it is read by what it means for the request — a
99
+ // refused credential is this client's session, anything else the relay's.
100
+ if (err instanceof RelayLoginError) {
101
+ const status = err.loginStatus;
102
+ const kind = err.kind === 'signed-out' ? 'unauthorized' : status ? kindForStatus(status) : 'unreachable';
103
+ return { kind, status, retryable: RETRYABLE[kind], message: messageOf(err) };
104
+ }
109
105
  const status = statusOf(err);
110
106
  let kind;
111
107
  if (status !== undefined) {
@@ -1,6 +1,6 @@
1
1
  export * from './http-types.js';
2
2
  export * from './api-response.js';
3
- export { WebHttpClient, HTTPError, AuthRevokedError } from './web-client.js';
3
+ export { WebHttpClient, HTTPError, AuthRevokedError, RequestAuthError } from './web-client.js';
4
4
  export { createHttpClient, createBrowserHttpClient, createNodeHttpClient, createUniversalHttpClient, } from './http-factory.js';
5
5
  export { withRetry, createRetryableMethod } from './retry.js';
6
6
  export type { RetryOptions } from './retry.js';
@@ -2,7 +2,7 @@
2
2
  export * from './http-types.js';
3
3
  export * from './api-response.js';
4
4
  // Web Standards HTTP client implementation
5
- export { WebHttpClient, HTTPError, AuthRevokedError } from './web-client.js';
5
+ export { WebHttpClient, HTTPError, AuthRevokedError, RequestAuthError } from './web-client.js';
6
6
  // Factory functions for easy client creation
7
7
  export { createHttpClient, createBrowserHttpClient, createNodeHttpClient, createUniversalHttpClient, } from './http-factory.js';
8
8
  // Retry functionality
@@ -36,6 +36,19 @@ 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
+ /**
40
+ * A failure of a transport's `getAuthToken` that must stop the request.
41
+ *
42
+ * Any other error from `getAuthToken` is ignored and the request goes out
43
+ * without a token, which is right for a credential that is merely absent. This
44
+ * one says the credential could not be obtained at all — the login it needed
45
+ * was refused, or never reached the server — so sending the request anyway
46
+ * would only trade the real cause for the server's 401, or for the same
47
+ * transport error a second time. Thrown as is from the request.
48
+ */
49
+ export declare class RequestAuthError extends Error {
50
+ name: string;
51
+ }
39
52
  /** Whether `headers` carry a request proof, which must never follow a redirect. */
40
53
  export declare function carriesProof(headers: Record<string, string>): boolean;
41
54
  /**
@@ -87,6 +87,22 @@ export class AuthRevokedError extends HTTPError {
87
87
  this.message = `Authentication revoked (${reason}): HTTP ${status} ${statusText}`;
88
88
  }
89
89
  }
90
+ /**
91
+ * A failure of a transport's `getAuthToken` that must stop the request.
92
+ *
93
+ * Any other error from `getAuthToken` is ignored and the request goes out
94
+ * without a token, which is right for a credential that is merely absent. This
95
+ * one says the credential could not be obtained at all — the login it needed
96
+ * was refused, or never reached the server — so sending the request anyway
97
+ * would only trade the real cause for the server's 401, or for the same
98
+ * transport error a second time. Thrown as is from the request.
99
+ */
100
+ export class RequestAuthError extends Error {
101
+ constructor() {
102
+ super(...arguments);
103
+ this.name = 'RequestAuthError';
104
+ }
105
+ }
90
106
  // Helper function to convert Headers to Record
91
107
  function headersToRecord(headers) {
92
108
  const record = {};
@@ -477,7 +493,10 @@ export class WebHttpClient {
477
493
  }
478
494
  }
479
495
  catch (error) {
480
- // Ignore auth token errors
496
+ // A credential that could not be obtained stops the request; any other
497
+ // failure leaves it unauthenticated, as before.
498
+ if (error instanceof RequestAuthError)
499
+ throw error;
481
500
  }
482
501
  }
483
502
  // Add init headers