@calimero-network/mero-js 26.5.0 → 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.
@@ -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';
@@ -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
+ }
@@ -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;
@@ -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