@calimero-network/mero-js 21.2.1 → 21.3.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/index.cjs CHANGED
@@ -49,6 +49,7 @@ __export(index_exports, {
49
49
  admitterAddrsFromNetworkStatus: () => admitterAddrsFromNetworkStatus,
50
50
  buildAuthLoginUrl: () => buildAuthLoginUrl,
51
51
  callerProof: () => callerProof,
52
+ classifyError: () => classifyError,
52
53
  combineSignals: () => combineSignals,
53
54
  compareSemver: () => compareSemver,
54
55
  connectCloud: () => connectCloud,
@@ -82,6 +83,7 @@ __export(index_exports, {
82
83
  hasCap: () => hasCap,
83
84
  hexEncodeUtf8: () => hexEncodeUtf8,
84
85
  intentHash: () => intentHash,
86
+ isSettled: () => isSettled,
85
87
  jsonCodec: () => jsonCodec,
86
88
  localNodeUrl: () => localNodeUrl,
87
89
  login: () => login,
@@ -89,8 +91,10 @@ __export(index_exports, {
89
91
  nodeEndpoint: () => nodeEndpoint,
90
92
  parseAuthCallback: () => parseAuthCallback,
91
93
  probeNodeHealth: () => probeNodeHealth,
94
+ redeemInvitation: () => redeemInvitation,
92
95
  requestBodyHash: () => requestBodyHash,
93
96
  routingProofHeaders: () => routingProofHeaders,
97
+ shouldRetain: () => shouldRetain,
94
98
  signAccountLink: () => signAccountLink,
95
99
  signAccountLogin: () => signAccountLogin,
96
100
  signDeviceCert: () => signDeviceCert,
@@ -168,6 +172,7 @@ var HTTPError = class extends Error {
168
172
  this.headers = headers;
169
173
  this.bodyText = bodyText;
170
174
  this.name = "HTTPError";
175
+ this.explanation = explanation;
171
176
  }
172
177
  toJSON() {
173
178
  return {
@@ -864,6 +869,145 @@ function withoutCap(mask, cap) {
864
869
  return (mask & ~cap) >>> 0;
865
870
  }
866
871
 
872
+ // src/errors/classify.ts
873
+ var RETRYABLE = {
874
+ invalid: false,
875
+ // The session, not the request: signing in again and repeating it works.
876
+ unauthorized: true,
877
+ forbidden: false,
878
+ // Too broad to give up on — the resource may simply not have synced yet.
879
+ "not-found": true,
880
+ conflict: false,
881
+ gone: false,
882
+ "too-large": false,
883
+ "rate-limited": true,
884
+ unavailable: true,
885
+ server: true,
886
+ unreachable: true,
887
+ unknown: true
888
+ };
889
+ function kindForStatus(status) {
890
+ switch (status) {
891
+ case 0:
892
+ return "unreachable";
893
+ case 400:
894
+ return "invalid";
895
+ case 401:
896
+ return "unauthorized";
897
+ case 403:
898
+ return "forbidden";
899
+ case 404:
900
+ return "not-found";
901
+ case 409:
902
+ return "conflict";
903
+ case 410:
904
+ return "gone";
905
+ case 413:
906
+ return "too-large";
907
+ case 429:
908
+ return "rate-limited";
909
+ case 503:
910
+ case 504:
911
+ return "unavailable";
912
+ }
913
+ if (status >= 500 && status < 600) return "server";
914
+ if (status >= 400 && status < 500) return "invalid";
915
+ return "unknown";
916
+ }
917
+ function statusOf(err) {
918
+ if (!err || typeof err !== "object") return void 0;
919
+ const shaped = err;
920
+ for (const candidate of [shaped.status, shaped.response?.status, shaped.statusCode]) {
921
+ if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate;
922
+ }
923
+ return void 0;
924
+ }
925
+ function messageOf(err, fallback = "Request failed") {
926
+ if (typeof err === "string") return err.trim() || fallback;
927
+ if (!err || typeof err !== "object") return fallback;
928
+ const shaped = err;
929
+ for (const candidate of [
930
+ shaped.explanation,
931
+ shaped.response?.data?.error,
932
+ shaped.response?.data?.message,
933
+ shaped.data?.message,
934
+ shaped.error,
935
+ shaped.message
936
+ ]) {
937
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
938
+ }
939
+ return fallback;
940
+ }
941
+ function classifyError(err) {
942
+ const status = statusOf(err);
943
+ let kind;
944
+ if (status !== void 0) {
945
+ kind = kindForStatus(status);
946
+ } else if (err instanceof TypeError) {
947
+ kind = "unreachable";
948
+ } else {
949
+ kind = "unknown";
950
+ }
951
+ return { kind, status, retryable: RETRYABLE[kind], message: messageOf(err) };
952
+ }
953
+
954
+ // src/admin-api/redeem-invitation.ts
955
+ function isSettled(outcome) {
956
+ return outcome.status !== "failed" || !outcome.retryable;
957
+ }
958
+ function shouldRetain(outcome) {
959
+ return !isSettled(outcome);
960
+ }
961
+ function reasonFromMessage(message) {
962
+ const m = message.toLowerCase();
963
+ if (m.includes("invitation has expired") || m.includes("invitation expired") || // Core rc.56+ names the group between the words: "invitation for group
964
+ // <id> expired at <secs>".
965
+ /\binvitation for group .+? expired at \d+/.test(m)) {
966
+ return "expired";
967
+ }
968
+ if (m.includes("malformed") || m.includes("does not match namespace_id") || m.includes("invalid invitation") || /\binvitation for group .+? is invalid:/.test(m)) {
969
+ return "invalid";
970
+ }
971
+ return null;
972
+ }
973
+ function classifyJoinFailure(err, message) {
974
+ const { kind, status } = classifyError(err);
975
+ const fromText = reasonFromMessage(message);
976
+ if (status === 401) return { reason: "signed-out", retryable: true };
977
+ if (status === 400) return { reason: fromText ?? "invalid", retryable: false };
978
+ if (status === 410) return { reason: "expired", retryable: false };
979
+ if (status === 403) return { reason: "refused", retryable: false };
980
+ if (status === 409) return { reason: fromText ?? "refused", retryable: false };
981
+ if (kind === "unavailable") return { reason: "no-one-online", retryable: true };
982
+ if (kind === "unreachable") return { reason: "node-unreachable", retryable: true };
983
+ if (fromText) return { reason: fromText, retryable: false };
984
+ return { reason: "unknown", retryable: true };
985
+ }
986
+ async function redeemInvitation(parsed, redeemer) {
987
+ const { namespaceId, invitation, teamName } = parsed;
988
+ let joinError = null;
989
+ try {
990
+ await redeemer.join(namespaceId, invitation);
991
+ } catch (err) {
992
+ joinError = err;
993
+ }
994
+ let listed = null;
995
+ try {
996
+ listed = (await redeemer.memberships()).includes(namespaceId);
997
+ } catch {
998
+ listed = null;
999
+ }
1000
+ if (joinError === null) {
1001
+ return { status: "joined", namespaceId, teamName };
1002
+ }
1003
+ if (listed === true) {
1004
+ return { status: "already-member", namespaceId, teamName };
1005
+ }
1006
+ const message = messageOf(joinError, "Could not join. Check the invitation.");
1007
+ const { reason, retryable } = classifyJoinFailure(joinError, message);
1008
+ return { status: "failed", namespaceId, message, reason, retryable };
1009
+ }
1010
+
867
1011
  // src/admin-api/admin-client.ts
868
1012
  function unwrap(response) {
869
1013
  return response?.data;
@@ -1441,6 +1585,40 @@ var AdminApiClient = class {
1441
1585
  }
1442
1586
  return data;
1443
1587
  }
1588
+ /**
1589
+ * Join the namespace an invitation grants, and say what happened — including
1590
+ * when the request failed but the join landed anyway.
1591
+ *
1592
+ * Use this rather than {@link joinNamespace} whenever someone is following an
1593
+ * invitation. A join waits for a member to come online and can outlast any
1594
+ * proxy in front of the node; the request then fails while the join
1595
+ * succeeds. This sends the join once, then checks {@link listNamespaces}:
1596
+ * a failed request whose namespace is listed is `already-member`, a success.
1597
+ *
1598
+ * Never throws for an ordinary failure. A failed outcome carries a `reason`
1599
+ * read off the node's status (core rc.56+), and `retryable`, which says
1600
+ * whether to keep the invitation for another attempt.
1601
+ *
1602
+ * @param namespaceId the namespace the invitation names, 64 hex
1603
+ * @param invitation the signed invitation, as the inviter's node minted it
1604
+ * @param options `groupName` is sent with the join; `teamName` is echoed back
1605
+ * on a successful outcome for the caller's UI
1606
+ */
1607
+ async redeemInvitation(namespaceId, invitation, options = {}) {
1608
+ const { groupName, teamName } = options;
1609
+ return redeemInvitation(
1610
+ { namespaceId, invitation, teamName },
1611
+ {
1612
+ join: async () => {
1613
+ await this.joinNamespace(
1614
+ namespaceId,
1615
+ groupName === void 0 ? { invitation } : { invitation, groupName }
1616
+ );
1617
+ },
1618
+ memberships: async () => (await this.listNamespaces() ?? []).map((n) => n.namespaceId)
1619
+ }
1620
+ );
1621
+ }
1444
1622
  async createGroupInNamespace(namespaceId, request) {
1445
1623
  return unwrap(
1446
1624
  await this.httpClient.post(
@@ -7312,7 +7490,7 @@ function tcbStatus(status, release) {
7312
7490
  }
7313
7491
 
7314
7492
  // src/namespace-op/namespace-op.ts
7315
- var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 8;
7493
+ var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 9;
7316
7494
  var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");
7317
7495
  var NAMESPACE_OP_ROOT = 0;
7318
7496
  var ROOT_OP = {