@calimero-network/mero-js 26.0.0 → 26.1.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/join-with-node.d.ts +7 -12
- package/dist/account/join-with-node.js +7 -12
- package/dist/admin-api/redeem-invitation.d.ts +8 -1
- package/dist/admin-api/redeem-invitation.js +64 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +27 -0
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +27 -0
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
|
@@ -21,18 +21,13 @@
|
|
|
21
21
|
* `signer == credential.statement.sign_pk` before applying a join — so an
|
|
22
22
|
* admitter can carry this and can never author it: it cannot admit a different
|
|
23
23
|
* account, change the group, or grant a role. It can refuse, and that is all.
|
|
24
|
-
* This is also why the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* shape that is actually useful to a caller is the one that hands back a
|
|
32
|
-
* {@link DelegatedSession} — the node that admitted you is the node to connect
|
|
33
|
-
* through — which is `mero-react`'s concern. The pieces that are pure SDK, the
|
|
34
|
-
* borsh op layout and the request type, are used from `mero-js` unchanged and
|
|
35
|
-
* are not reimplemented here.
|
|
24
|
+
* This is also why core serves the route without a credential (it sits on the
|
|
25
|
+
* public admin router, beside `/health` and the intent routes, on every node
|
|
26
|
+
* whatever its auth mode), why the fleet ingress exempts the one anchored path,
|
|
27
|
+
* and why this posts with `fetch` rather than through `AdminApiClient`, which is
|
|
28
|
+
* built around a node credential this caller does not have. A node that predates
|
|
29
|
+
* that change and runs embedded auth answers 401 here; the failure is reported at
|
|
30
|
+
* the `admit` step like any other refusal.
|
|
36
31
|
*
|
|
37
32
|
* ## Why the op has no parents
|
|
38
33
|
*
|
|
@@ -21,18 +21,13 @@
|
|
|
21
21
|
* `signer == credential.statement.sign_pk` before applying a join — so an
|
|
22
22
|
* admitter can carry this and can never author it: it cannot admit a different
|
|
23
23
|
* account, change the group, or grant a role. It can refuse, and that is all.
|
|
24
|
-
* This is also why the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* shape that is actually useful to a caller is the one that hands back a
|
|
32
|
-
* {@link DelegatedSession} — the node that admitted you is the node to connect
|
|
33
|
-
* through — which is `mero-react`'s concern. The pieces that are pure SDK, the
|
|
34
|
-
* borsh op layout and the request type, are used from `mero-js` unchanged and
|
|
35
|
-
* are not reimplemented here.
|
|
24
|
+
* This is also why core serves the route without a credential (it sits on the
|
|
25
|
+
* public admin router, beside `/health` and the intent routes, on every node
|
|
26
|
+
* whatever its auth mode), why the fleet ingress exempts the one anchored path,
|
|
27
|
+
* and why this posts with `fetch` rather than through `AdminApiClient`, which is
|
|
28
|
+
* built around a node credential this caller does not have. A node that predates
|
|
29
|
+
* that change and runs embedded auth answers 401 here; the failure is reported at
|
|
30
|
+
* the `admit` step like any other refusal.
|
|
36
31
|
*
|
|
37
32
|
* ## Why the op has no parents
|
|
38
33
|
*
|
|
@@ -27,7 +27,14 @@ export type InviteFailureReason =
|
|
|
27
27
|
'expired'
|
|
28
28
|
/** The link is malformed or names nothing joinable. Only a new link helps. */
|
|
29
29
|
| 'invalid'
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* This person already joined with this link once, and has since left. A link
|
|
32
|
+
* admits each person once: only a new one helps.
|
|
33
|
+
*/
|
|
34
|
+
| 'already-used'
|
|
35
|
+
/** An admin removed this person. No link readmits them; an admin must re-add them. */
|
|
36
|
+
| 'removed'
|
|
37
|
+
/** The node refused this person for another reason (blocked, not allowed). */
|
|
31
38
|
| 'refused'
|
|
32
39
|
/** This client's session with the node lapsed; signing in finishes it. */
|
|
33
40
|
| 'signed-out'
|
|
@@ -28,9 +28,46 @@ export function isSettled(outcome) {
|
|
|
28
28
|
export function shouldRetain(outcome) {
|
|
29
29
|
return !isSettled(outcome);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The refusal codes a join route answers with beside `error` (core's
|
|
33
|
+
* `join_refusal_code`), and what each one tells a person.
|
|
34
|
+
*
|
|
35
|
+
* `join_refused_by_member` is absent on purpose: a member that refused may only
|
|
36
|
+
* be behind, so it says nothing final on its own, and the member's own words
|
|
37
|
+
* (in the message) are read by {@link reasonFromMessage} instead.
|
|
38
|
+
*/
|
|
39
|
+
const REASON_FOR_CODE = {
|
|
40
|
+
invitation_expired: 'expired',
|
|
41
|
+
invitation_invalid: 'invalid',
|
|
42
|
+
invitation_already_used: 'already-used',
|
|
43
|
+
removed_from_group: 'removed',
|
|
44
|
+
reentry_blocked: 'refused',
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The machine-readable `code` a node put beside its `error`, if any: on
|
|
48
|
+
* mero-js's `HTTPError` it is in the body text, on an axios-style error in
|
|
49
|
+
* `response.data`.
|
|
50
|
+
*/
|
|
51
|
+
function refusalCodeOf(err) {
|
|
52
|
+
if (!err || typeof err !== 'object')
|
|
53
|
+
return undefined;
|
|
54
|
+
const shaped = err;
|
|
55
|
+
if (typeof shaped.response?.data?.code === 'string')
|
|
56
|
+
return shaped.response.data.code;
|
|
57
|
+
if (typeof shaped.bodyText !== 'string')
|
|
58
|
+
return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const body = JSON.parse(shaped.bodyText);
|
|
61
|
+
return typeof body?.code === 'string' ? body.code : undefined;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
31
67
|
/**
|
|
32
68
|
* Which reason the message alone names, for a node that answers without a
|
|
33
|
-
* useful status — older than rc.56, where every refusal was a bare 500
|
|
69
|
+
* useful status — older than rc.56, where every refusal was a bare 500 — or
|
|
70
|
+
* without a `code`, or for a member's refusal quoted inside a retryable answer.
|
|
34
71
|
*
|
|
35
72
|
* Kept narrow on purpose. Anything not recognised here stays retryable, because
|
|
36
73
|
* the cost of retrying a dead link is one more attempt, while the cost of
|
|
@@ -51,8 +88,19 @@ function reasonFromMessage(message) {
|
|
|
51
88
|
/\binvitation for group .+? is invalid:/.test(m)) {
|
|
52
89
|
return 'invalid';
|
|
53
90
|
}
|
|
91
|
+
// Core's own words for a spent link and a removal (`MembershipError`), which a
|
|
92
|
+
// refusing member also quotes back to the joiner.
|
|
93
|
+
if (m.includes('has already used this invitation'))
|
|
94
|
+
return 'already-used';
|
|
95
|
+
if (/\bwas removed from group\b/.test(m))
|
|
96
|
+
return 'removed';
|
|
54
97
|
return null;
|
|
55
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The reasons that are final whatever status carried them: no later attempt
|
|
101
|
+
* with this link can succeed for this person.
|
|
102
|
+
*/
|
|
103
|
+
const FINAL_FROM_TEXT = new Set(['already-used', 'removed']);
|
|
56
104
|
/**
|
|
57
105
|
* Why a join failed, and whether a later attempt could work.
|
|
58
106
|
*
|
|
@@ -65,6 +113,21 @@ function reasonFromMessage(message) {
|
|
|
65
113
|
function classifyJoinFailure(err, message) {
|
|
66
114
|
const { kind, status } = classifyError(err);
|
|
67
115
|
const fromText = reasonFromMessage(message);
|
|
116
|
+
// A code is the node naming the refusal itself: nothing below can know better.
|
|
117
|
+
const code = refusalCodeOf(err);
|
|
118
|
+
const fromCode = code ? REASON_FOR_CODE[code] : undefined;
|
|
119
|
+
if (fromCode)
|
|
120
|
+
return { reason: fromCode, retryable: false };
|
|
121
|
+
// A spent link or a removal is final even when a member said it inside a
|
|
122
|
+
// retryable answer (a 503 for a join every peer refused): asking again only
|
|
123
|
+
// asks the same members, who will say the same.
|
|
124
|
+
if (fromText && FINAL_FROM_TEXT.has(fromText))
|
|
125
|
+
return { reason: fromText, retryable: false };
|
|
126
|
+
// Any other member refusal is not "no one is online": someone answered, and
|
|
127
|
+
// their words are the message. Kept, since a member behind on the team's
|
|
128
|
+
// history refuses what it has not seen yet.
|
|
129
|
+
if (code === 'join_refused_by_member')
|
|
130
|
+
return { reason: 'unknown', retryable: true };
|
|
68
131
|
if (status === 401)
|
|
69
132
|
return { reason: 'signed-out', retryable: true };
|
|
70
133
|
if (status === 400)
|