@calimero-network/mero-js 26.5.0 → 26.7.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.
- package/dist/account/index.d.ts +2 -0
- package/dist/account/index.js +1 -0
- package/dist/account/relay-login-error.d.ts +32 -0
- package/dist/account/relay-login-error.js +34 -0
- package/dist/account/session.js +8 -5
- package/dist/admin-api/admin-client.d.ts +16 -2
- package/dist/admin-api/admin-client.js +18 -3
- package/dist/admin-api/admin-types.d.ts +8 -0
- package/dist/admin-api/redeem-invitation.js +6 -0
- package/dist/errors/classify.js +9 -13
- package/dist/http-client/index.d.ts +1 -1
- package/dist/http-client/index.js +1 -1
- package/dist/http-client/web-client.d.ts +13 -0
- package/dist/http-client/web-client.js +20 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +66 -5
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +66 -5
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
package/dist/account/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/account/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/account/session.js
CHANGED
|
@@ -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
|
|
521
|
-
*
|
|
522
|
-
*
|
|
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
|
-
|
|
551
|
+
throw new RelayLoginError(s.relayUrl, e);
|
|
549
552
|
})
|
|
550
553
|
.finally(() => {
|
|
551
554
|
inflight = null;
|
|
@@ -219,6 +219,13 @@ export declare class AdminApiClient {
|
|
|
219
219
|
*/
|
|
220
220
|
admitJoin(namespaceId: string, request: AdmitJoinRequest): Promise<AdmitJoinResponseData>;
|
|
221
221
|
joinNamespace(namespaceId: string, request: JoinNamespaceRequest): Promise<JoinNamespaceResponseData>;
|
|
222
|
+
/**
|
|
223
|
+
* Bring back a namespace the user removed from this node while the account
|
|
224
|
+
* stayed a member (POST /admin-api/namespaces/:namespace_id/join, no
|
|
225
|
+
* invitation). Such namespaces are the `forgotten` ones in
|
|
226
|
+
* {@link listNamespacesForApplication}.
|
|
227
|
+
*/
|
|
228
|
+
restoreNamespace(namespaceId: string): Promise<JoinNamespaceResponseData>;
|
|
222
229
|
/**
|
|
223
230
|
* Join the namespace an invitation grants, and say what happened — including
|
|
224
231
|
* when the request failed but the join landed anyway.
|
|
@@ -623,8 +630,15 @@ export declare class AdminApiClient {
|
|
|
623
630
|
leaveGroup(groupId: string): Promise<void>;
|
|
624
631
|
/** Leave a context (POST /admin-api/contexts/:context_id/leave). */
|
|
625
632
|
leaveContext(contextId: string): Promise<void>;
|
|
626
|
-
/**
|
|
627
|
-
|
|
633
|
+
/**
|
|
634
|
+
* Leave a namespace (POST /admin-api/namespaces/:namespace_id/leave).
|
|
635
|
+
*
|
|
636
|
+
* With `purge`, every device of the account also takes the namespace off its
|
|
637
|
+
* disk. Core reads `purge` from the query string, so it is not in the body.
|
|
638
|
+
*/
|
|
639
|
+
leaveNamespace(namespaceId: string, options?: {
|
|
640
|
+
purge?: boolean;
|
|
641
|
+
}): Promise<void>;
|
|
628
642
|
/** Issue a group ownership proof (POST /admin-api/groups/:group_id/issue-ownership-proof). */
|
|
629
643
|
issueOwnershipProof(groupId: string, request: IssueOwnershipProofRequest): Promise<IssueOwnershipProofResponseData>;
|
|
630
644
|
/** Issue a namespace ownership proof (POST /admin-api/groups/:group_id/issue-namespace-ownership-proof). */
|
|
@@ -523,6 +523,15 @@ export class AdminApiClient {
|
|
|
523
523
|
}
|
|
524
524
|
return data;
|
|
525
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Bring back a namespace the user removed from this node while the account
|
|
528
|
+
* stayed a member (POST /admin-api/namespaces/:namespace_id/join, no
|
|
529
|
+
* invitation). Such namespaces are the `forgotten` ones in
|
|
530
|
+
* {@link listNamespacesForApplication}.
|
|
531
|
+
*/
|
|
532
|
+
async restoreNamespace(namespaceId) {
|
|
533
|
+
return unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/join`, {}, { timeoutMs: 65000 }));
|
|
534
|
+
}
|
|
526
535
|
/**
|
|
527
536
|
* Join the namespace an invitation grants, and say what happened — including
|
|
528
537
|
* when the request failed but the join landed anyway.
|
|
@@ -1137,9 +1146,15 @@ export class AdminApiClient {
|
|
|
1137
1146
|
async leaveContext(contextId) {
|
|
1138
1147
|
await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/leave`, {});
|
|
1139
1148
|
}
|
|
1140
|
-
/**
|
|
1141
|
-
|
|
1142
|
-
|
|
1149
|
+
/**
|
|
1150
|
+
* Leave a namespace (POST /admin-api/namespaces/:namespace_id/leave).
|
|
1151
|
+
*
|
|
1152
|
+
* With `purge`, every device of the account also takes the namespace off its
|
|
1153
|
+
* disk. Core reads `purge` from the query string, so it is not in the body.
|
|
1154
|
+
*/
|
|
1155
|
+
async leaveNamespace(namespaceId, options = {}) {
|
|
1156
|
+
const query = options.purge ? '?purge=true' : '';
|
|
1157
|
+
await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/leave${query}`, {});
|
|
1143
1158
|
}
|
|
1144
1159
|
/** Issue a group ownership proof (POST /admin-api/groups/:group_id/issue-ownership-proof). */
|
|
1145
1160
|
async issueOwnershipProof(groupId, request) {
|
|
@@ -377,6 +377,12 @@ export interface Namespace {
|
|
|
377
377
|
* current on another. Absent when nothing is held, and on nodes that predate it.
|
|
378
378
|
*/
|
|
379
379
|
heldOps?: NamespaceHeldOps;
|
|
380
|
+
/**
|
|
381
|
+
* The user removed this namespace from this node and the account is still a
|
|
382
|
+
* member. Set only by {@link AdminApiClient.listNamespacesForApplication};
|
|
383
|
+
* {@link AdminApiClient.restoreNamespace} brings it back.
|
|
384
|
+
*/
|
|
385
|
+
forgotten?: boolean;
|
|
380
386
|
}
|
|
381
387
|
/** The group ops a namespace holds unapplied on the node that answered. */
|
|
382
388
|
export interface NamespaceHeldOps {
|
|
@@ -669,6 +675,8 @@ export interface JoinNamespaceResponseData {
|
|
|
669
675
|
* is how the caller addresses the member it just became.
|
|
670
676
|
*/
|
|
671
677
|
memberAccount: string;
|
|
678
|
+
/** The name a restored namespace had when it was removed. Set only by a restore. */
|
|
679
|
+
name?: string;
|
|
672
680
|
}
|
|
673
681
|
export interface CreateGroupInNamespaceRequest {
|
|
674
682
|
groupName?: string;
|
|
@@ -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.
|
package/dist/errors/classify.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
//
|
|
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
|