@calimero-network/mero-js 21.2.2 → 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.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { MeroJsConfig, TokenData } from "./mero-js.js";
3
3
  export * from "./http-client/index.js";
4
4
  export * from "./auth-api/index.js";
5
5
  export * from "./admin-api/index.js";
6
+ export * from "./errors/index.js";
6
7
  export { parseAuthCallback, buildAuthLoginUrl } from "./auth/index.js";
7
8
  export type { AuthCallbackResult, AuthLoginOptions } from "./auth/index.js";
8
9
  export { MemoryTokenStore, LocalStorageTokenStore, } from "./token-store/index.js";
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ export * from "./http-client/index.js";
8
8
  export * from "./auth-api/index.js";
9
9
  // Admin API client
10
10
  export * from "./admin-api/index.js";
11
+ // What a failed request means — kind, retryable, the node's own words
12
+ export * from "./errors/index.js";
11
13
  // Auth utilities
12
14
  export { parseAuthCallback, buildAuthLoginUrl } from "./auth/index.js";
13
15
  // Token store
package/dist/index.mjs CHANGED
@@ -59,6 +59,7 @@ var HTTPError = class extends Error {
59
59
  this.headers = headers;
60
60
  this.bodyText = bodyText;
61
61
  this.name = "HTTPError";
62
+ this.explanation = explanation;
62
63
  }
63
64
  toJSON() {
64
65
  return {
@@ -755,6 +756,145 @@ function withoutCap(mask, cap) {
755
756
  return (mask & ~cap) >>> 0;
756
757
  }
757
758
 
759
+ // src/errors/classify.ts
760
+ var RETRYABLE = {
761
+ invalid: false,
762
+ // The session, not the request: signing in again and repeating it works.
763
+ unauthorized: true,
764
+ forbidden: false,
765
+ // Too broad to give up on — the resource may simply not have synced yet.
766
+ "not-found": true,
767
+ conflict: false,
768
+ gone: false,
769
+ "too-large": false,
770
+ "rate-limited": true,
771
+ unavailable: true,
772
+ server: true,
773
+ unreachable: true,
774
+ unknown: true
775
+ };
776
+ function kindForStatus(status) {
777
+ switch (status) {
778
+ case 0:
779
+ return "unreachable";
780
+ case 400:
781
+ return "invalid";
782
+ case 401:
783
+ return "unauthorized";
784
+ case 403:
785
+ return "forbidden";
786
+ case 404:
787
+ return "not-found";
788
+ case 409:
789
+ return "conflict";
790
+ case 410:
791
+ return "gone";
792
+ case 413:
793
+ return "too-large";
794
+ case 429:
795
+ return "rate-limited";
796
+ case 503:
797
+ case 504:
798
+ return "unavailable";
799
+ }
800
+ if (status >= 500 && status < 600) return "server";
801
+ if (status >= 400 && status < 500) return "invalid";
802
+ return "unknown";
803
+ }
804
+ function statusOf(err) {
805
+ if (!err || typeof err !== "object") return void 0;
806
+ const shaped = err;
807
+ for (const candidate of [shaped.status, shaped.response?.status, shaped.statusCode]) {
808
+ if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate;
809
+ }
810
+ return void 0;
811
+ }
812
+ function messageOf(err, fallback = "Request failed") {
813
+ if (typeof err === "string") return err.trim() || fallback;
814
+ if (!err || typeof err !== "object") return fallback;
815
+ const shaped = err;
816
+ for (const candidate of [
817
+ shaped.explanation,
818
+ shaped.response?.data?.error,
819
+ shaped.response?.data?.message,
820
+ shaped.data?.message,
821
+ shaped.error,
822
+ shaped.message
823
+ ]) {
824
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
825
+ }
826
+ return fallback;
827
+ }
828
+ function classifyError(err) {
829
+ const status = statusOf(err);
830
+ let kind;
831
+ if (status !== void 0) {
832
+ kind = kindForStatus(status);
833
+ } else if (err instanceof TypeError) {
834
+ kind = "unreachable";
835
+ } else {
836
+ kind = "unknown";
837
+ }
838
+ return { kind, status, retryable: RETRYABLE[kind], message: messageOf(err) };
839
+ }
840
+
841
+ // src/admin-api/redeem-invitation.ts
842
+ function isSettled(outcome) {
843
+ return outcome.status !== "failed" || !outcome.retryable;
844
+ }
845
+ function shouldRetain(outcome) {
846
+ return !isSettled(outcome);
847
+ }
848
+ function reasonFromMessage(message) {
849
+ const m = message.toLowerCase();
850
+ if (m.includes("invitation has expired") || m.includes("invitation expired") || // Core rc.56+ names the group between the words: "invitation for group
851
+ // <id> expired at <secs>".
852
+ /\binvitation for group .+? expired at \d+/.test(m)) {
853
+ return "expired";
854
+ }
855
+ if (m.includes("malformed") || m.includes("does not match namespace_id") || m.includes("invalid invitation") || /\binvitation for group .+? is invalid:/.test(m)) {
856
+ return "invalid";
857
+ }
858
+ return null;
859
+ }
860
+ function classifyJoinFailure(err, message) {
861
+ const { kind, status } = classifyError(err);
862
+ const fromText = reasonFromMessage(message);
863
+ if (status === 401) return { reason: "signed-out", retryable: true };
864
+ if (status === 400) return { reason: fromText ?? "invalid", retryable: false };
865
+ if (status === 410) return { reason: "expired", retryable: false };
866
+ if (status === 403) return { reason: "refused", retryable: false };
867
+ if (status === 409) return { reason: fromText ?? "refused", retryable: false };
868
+ if (kind === "unavailable") return { reason: "no-one-online", retryable: true };
869
+ if (kind === "unreachable") return { reason: "node-unreachable", retryable: true };
870
+ if (fromText) return { reason: fromText, retryable: false };
871
+ return { reason: "unknown", retryable: true };
872
+ }
873
+ async function redeemInvitation(parsed, redeemer) {
874
+ const { namespaceId, invitation, teamName } = parsed;
875
+ let joinError = null;
876
+ try {
877
+ await redeemer.join(namespaceId, invitation);
878
+ } catch (err) {
879
+ joinError = err;
880
+ }
881
+ let listed = null;
882
+ try {
883
+ listed = (await redeemer.memberships()).includes(namespaceId);
884
+ } catch {
885
+ listed = null;
886
+ }
887
+ if (joinError === null) {
888
+ return { status: "joined", namespaceId, teamName };
889
+ }
890
+ if (listed === true) {
891
+ return { status: "already-member", namespaceId, teamName };
892
+ }
893
+ const message = messageOf(joinError, "Could not join. Check the invitation.");
894
+ const { reason, retryable } = classifyJoinFailure(joinError, message);
895
+ return { status: "failed", namespaceId, message, reason, retryable };
896
+ }
897
+
758
898
  // src/admin-api/admin-client.ts
759
899
  function unwrap(response) {
760
900
  return response?.data;
@@ -1332,6 +1472,40 @@ var AdminApiClient = class {
1332
1472
  }
1333
1473
  return data;
1334
1474
  }
1475
+ /**
1476
+ * Join the namespace an invitation grants, and say what happened — including
1477
+ * when the request failed but the join landed anyway.
1478
+ *
1479
+ * Use this rather than {@link joinNamespace} whenever someone is following an
1480
+ * invitation. A join waits for a member to come online and can outlast any
1481
+ * proxy in front of the node; the request then fails while the join
1482
+ * succeeds. This sends the join once, then checks {@link listNamespaces}:
1483
+ * a failed request whose namespace is listed is `already-member`, a success.
1484
+ *
1485
+ * Never throws for an ordinary failure. A failed outcome carries a `reason`
1486
+ * read off the node's status (core rc.56+), and `retryable`, which says
1487
+ * whether to keep the invitation for another attempt.
1488
+ *
1489
+ * @param namespaceId the namespace the invitation names, 64 hex
1490
+ * @param invitation the signed invitation, as the inviter's node minted it
1491
+ * @param options `groupName` is sent with the join; `teamName` is echoed back
1492
+ * on a successful outcome for the caller's UI
1493
+ */
1494
+ async redeemInvitation(namespaceId, invitation, options = {}) {
1495
+ const { groupName, teamName } = options;
1496
+ return redeemInvitation(
1497
+ { namespaceId, invitation, teamName },
1498
+ {
1499
+ join: async () => {
1500
+ await this.joinNamespace(
1501
+ namespaceId,
1502
+ groupName === void 0 ? { invitation } : { invitation, groupName }
1503
+ );
1504
+ },
1505
+ memberships: async () => (await this.listNamespaces() ?? []).map((n) => n.namespaceId)
1506
+ }
1507
+ );
1508
+ }
1335
1509
  async createGroupInNamespace(namespaceId, request) {
1336
1510
  return unwrap(
1337
1511
  await this.httpClient.post(
@@ -7556,6 +7730,7 @@ export {
7556
7730
  admitterAddrsFromNetworkStatus,
7557
7731
  buildAuthLoginUrl,
7558
7732
  callerProof,
7733
+ classifyError,
7559
7734
  combineSignals,
7560
7735
  compareSemver,
7561
7736
  connectCloud,
@@ -7589,6 +7764,7 @@ export {
7589
7764
  hasCap,
7590
7765
  hexEncodeUtf8,
7591
7766
  intentHash,
7767
+ isSettled,
7592
7768
  jsonCodec,
7593
7769
  localNodeUrl,
7594
7770
  login,
@@ -7596,8 +7772,10 @@ export {
7596
7772
  nodeEndpoint,
7597
7773
  parseAuthCallback,
7598
7774
  probeNodeHealth,
7775
+ redeemInvitation,
7599
7776
  requestBodyHash,
7600
7777
  routingProofHeaders,
7778
+ shouldRetain,
7601
7779
  signAccountLink,
7602
7780
  signAccountLogin,
7603
7781
  signDeviceCert,