@calimero-network/mero-js 29.2.0 → 29.4.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
@@ -50,6 +50,7 @@ __export(index_exports, {
50
50
  IdempotencyMismatch: () => IdempotencyMismatch,
51
51
  KMS_RELEASE_SIGNER: () => KMS_RELEASE_SIGNER,
52
52
  LoginRequired: () => LoginRequired,
53
+ MAX_RECOVERY_NONCE_BYTES: () => MAX_RECOVERY_NONCE_BYTES,
53
54
  MAX_REPORT_PAGE: () => MAX_REPORT_PAGE,
54
55
  MOCK_QUOTE_HEADER: () => MOCK_QUOTE_HEADER,
55
56
  NODE_RELEASE_SIGNER: () => NODE_RELEASE_SIGNER,
@@ -59,11 +60,13 @@ __export(index_exports, {
59
60
  OriginTaken: () => OriginTaken,
60
61
  PRODUCTION_PROFILE: () => PRODUCTION_PROFILE,
61
62
  QuotaExceeded: () => QuotaExceeded,
63
+ RECOVERY_DOMAIN: () => RECOVERY_DOMAIN,
62
64
  RELEASE_VERIFIER: () => RELEASE_VERIFIER,
63
65
  ReleaseDeadlineError: () => ReleaseDeadlineError,
64
66
  SEALED_CONTENT_TYPE: () => SEALED_CONTENT_TYPE,
65
67
  SEALED_PATH: () => SEALED_PATH,
66
68
  SealedTransportError: () => SealedTransportError,
69
+ SignUpRefused: () => SignUpRefused,
67
70
  StaleTransportKeyError: () => StaleTransportKeyError,
68
71
  TrapError: () => TrapError,
69
72
  UnavailableError: () => UnavailableError,
@@ -91,18 +94,24 @@ __export(index_exports, {
91
94
  fromB64url: () => fromB64url,
92
95
  frontendManifest: () => frontendManifest,
93
96
  generateOwnerKey: () => generateOwnerKey,
97
+ newRecoverySeed: () => newRecoverySeed,
94
98
  orgCapabilities: () => orgCapabilities,
95
99
  ownerPublicKey: () => ownerPublicKey,
96
100
  pageDeployment: () => pageDeployment,
97
101
  pageQuery: () => pageQuery,
102
+ parseRecoverySeed: () => parseRecoverySeed,
98
103
  pendingApprovalsFrom: () => pendingApprovalsFrom,
99
104
  provisionAgent: () => provisionAgent,
100
105
  provisionInfo: () => provisionInfo,
101
106
  publisherBindingBytes: () => publisherBindingBytes,
102
107
  publisherBindingMessage: () => publisherBindingMessage,
108
+ recoveryMessage: () => recoveryMessage,
109
+ recoveryPublicKey: () => recoveryPublicKey,
110
+ recoverySeedText: () => recoverySeedText,
103
111
  releaseAgent: () => releaseAgent,
104
112
  releaseConsent: () => releaseConsent,
105
113
  releaseNotesUrl: () => releaseNotesUrl,
114
+ signRecovery: () => signRecovery,
106
115
  stalledBy: () => stalledBy,
107
116
  transportKeyBinding: () => transportKeyBinding,
108
117
  trustSignedRelease: () => trustSignedRelease,
@@ -253,20 +262,20 @@ async function x25519KeyPair(secret) {
253
262
  const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
254
263
  return { privateKey: pair.privateKey, publicKey };
255
264
  }
256
- const privateKey = await crypto.subtle.importKey(
265
+ const privateKey2 = await crypto.subtle.importKey(
257
266
  "pkcs8",
258
267
  concat(PKCS8_X25519_PREFIX, secret),
259
268
  { name: "X25519" },
260
269
  true,
261
270
  ["deriveBits"]
262
271
  );
263
- const jwk = await crypto.subtle.exportKey("jwk", privateKey);
264
- return { privateKey, publicKey: base64UrlDecode(jwk.x) };
272
+ const jwk = await crypto.subtle.exportKey("jwk", privateKey2);
273
+ return { privateKey: privateKey2, publicKey: base64UrlDecode(jwk.x) };
265
274
  }
266
- async function dh(privateKey, publicKey) {
275
+ async function dh(privateKey2, publicKey) {
267
276
  const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
268
277
  return new Uint8Array(
269
- await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey, 256)
278
+ await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey2, 256)
270
279
  );
271
280
  }
272
281
  function noiseNonce(n) {
@@ -1536,6 +1545,21 @@ var OriginTaken = class extends CalimeroError {
1536
1545
  this.name = "OriginTaken";
1537
1546
  }
1538
1547
  };
1548
+ var SignUpRefused = class extends CalimeroError {
1549
+ constructor(message, details = {}) {
1550
+ super(403, "SIGN_UP_REFUSED", message, details);
1551
+ this.name = "SignUpRefused";
1552
+ this.reason = signUpRefusal(message);
1553
+ }
1554
+ };
1555
+ function signUpRefusal(message) {
1556
+ if (/invite only/.test(message)) return "invite-only";
1557
+ if (/pre-authorized|sign-up invite/.test(message)) return "closed";
1558
+ if (/unknown invite/.test(message)) return "unknown-invite";
1559
+ if (/invite has expired/.test(message)) return "expired-invite";
1560
+ if (/organization is gone/.test(message)) return "org-gone";
1561
+ return "other";
1562
+ }
1539
1563
  var QuotaExceeded = class extends CalimeroError {
1540
1564
  constructor(message, details = {}) {
1541
1565
  super(429, "QUOTA_EXCEEDED", message, details);
@@ -1610,6 +1634,8 @@ function errorFrom(status, body, retryAfterHeader) {
1610
1634
  return new OriginTaken(message, details);
1611
1635
  case "429 QUOTA_EXCEEDED":
1612
1636
  return new QuotaExceeded(message, details);
1637
+ case "403 SIGN_UP_REFUSED":
1638
+ return new SignUpRefused(message, details);
1613
1639
  case "412 CONFIRM_REQUIRED":
1614
1640
  return new ConfirmRequired(message, details);
1615
1641
  case "413 ARGS_TOO_LARGE":
@@ -1686,6 +1712,72 @@ function browserPasskey() {
1686
1712
  };
1687
1713
  }
1688
1714
 
1715
+ // src/cloud-v2/recovery.ts
1716
+ var RECOVERY_DOMAIN = "calimero.org-recovery.v1\0";
1717
+ var MAX_RECOVERY_NONCE_BYTES = 64;
1718
+ var PKCS8_ED25519 = Uint8Array.from([
1719
+ 48,
1720
+ 46,
1721
+ 2,
1722
+ 1,
1723
+ 0,
1724
+ 48,
1725
+ 5,
1726
+ 6,
1727
+ 3,
1728
+ 43,
1729
+ 101,
1730
+ 112,
1731
+ 4,
1732
+ 34,
1733
+ 4,
1734
+ 32
1735
+ ]);
1736
+ function newRecoverySeed() {
1737
+ return crypto.getRandomValues(new Uint8Array(32));
1738
+ }
1739
+ function recoverySeedText(seed) {
1740
+ checkSeed(seed);
1741
+ const hex2 = Array.from(seed, (b) => b.toString(16).padStart(2, "0")).join("");
1742
+ const groups = [];
1743
+ for (let i = 0; i < hex2.length; i += 8) groups.push(hex2.slice(i, i + 8));
1744
+ return groups.join("-");
1745
+ }
1746
+ function parseRecoverySeed(text) {
1747
+ const digits = text.replace(/[-\s]/g, "");
1748
+ if (!/^[0-9a-fA-F]{64}$/.test(digits)) throw new Error("a recovery seed is 64 hex digits");
1749
+ return Uint8Array.from({ length: 32 }, (_, i) => parseInt(digits.slice(2 * i, 2 * i + 2), 16));
1750
+ }
1751
+ function recoveryMessage(org, account, nonce2) {
1752
+ return new TextEncoder().encode(`${RECOVERY_DOMAIN}${org}\0${account}\0${nonce2}`);
1753
+ }
1754
+ async function recoveryPublicKey(seed) {
1755
+ const key = await privateKey(seed, true);
1756
+ const jwk = await crypto.subtle.exportKey("jwk", key);
1757
+ if (!jwk.x) throw new Error("this runtime cannot export an Ed25519 public key");
1758
+ return jwk.x;
1759
+ }
1760
+ async function signRecovery(seed, org, account, nonce2) {
1761
+ const nonceBytes = new TextEncoder().encode(nonce2).length;
1762
+ if (nonceBytes === 0 || nonceBytes > MAX_RECOVERY_NONCE_BYTES) {
1763
+ throw new Error(`a recovery nonce is 1 to ${MAX_RECOVERY_NONCE_BYTES} bytes`);
1764
+ }
1765
+ const key = await privateKey(seed, false);
1766
+ const sig = await crypto.subtle.sign("Ed25519", key, buf2(recoveryMessage(org, account, nonce2)));
1767
+ return b64url(new Uint8Array(sig));
1768
+ }
1769
+ function checkSeed(seed) {
1770
+ if (seed.length !== 32) throw new Error("a recovery seed is 32 bytes");
1771
+ }
1772
+ function privateKey(seed, extractable) {
1773
+ checkSeed(seed);
1774
+ const pkcs8 = new Uint8Array(PKCS8_ED25519.length + 32);
1775
+ pkcs8.set(PKCS8_ED25519);
1776
+ pkcs8.set(seed, PKCS8_ED25519.length);
1777
+ return crypto.subtle.importKey("pkcs8", buf2(pkcs8), { name: "Ed25519" }, extractable, ["sign"]);
1778
+ }
1779
+ var buf2 = (b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
1780
+
1689
1781
  // src/cloud-v2/approvals.ts
1690
1782
  var MAX_REPORT_PAGE = 100;
1691
1783
  function pageQuery(page = {}) {
@@ -2178,23 +2270,33 @@ var CalimeroClient = class _CalimeroClient {
2178
2270
  * origin's relying party (the confirmation's `rpId` is that origin's too)
2179
2271
  * and binds the session to the package the origin is registered for; an
2180
2272
  * unregistered origin is refused with {@link OriginNotRegistered}.
2273
+ *
2274
+ * `invite` ({@link SignUpOptions}) goes on both halves and on the retry,
2275
+ * as the node judges it on each before it spends anything; a sign-up the
2276
+ * cluster's policy refuses, or an unknown, expired or used invite, is
2277
+ * {@link SignUpRefused} (from `begin`, before any passkey is made).
2181
2278
  */
2182
2279
  async signUp(passkey, name, options = {}) {
2183
2280
  const origin = this.pageOrigin(options);
2281
+ const invite = options.invite ? { invite: options.invite } : {};
2184
2282
  const begin = await this.begin(
2185
2283
  "auth/passkey/register/begin",
2186
- origin === void 0 ? { name } : { name, origin },
2284
+ origin === void 0 ? { name, ...invite } : { name, origin, ...invite },
2187
2285
  origin
2188
2286
  );
2189
2287
  const registration = await passkey.create(begin.options);
2190
2288
  const session = await this.postConfirmed(
2191
2289
  "auth/passkey/register/finish",
2192
- { challengeId: begin.challengeId, registration },
2290
+ { challengeId: begin.challengeId, registration, ...invite },
2193
2291
  passkey
2194
2292
  );
2195
2293
  this.useSession(session);
2196
2294
  return session;
2197
2295
  }
2296
+ /** Who may create an account on the cluster (`GET /sign-up`); needs no session. */
2297
+ signUpPolicy() {
2298
+ return this.get("sign-up");
2299
+ }
2198
2300
  /**
2199
2301
  * Sign in with an existing passkey. On a page whose origin is not the
2200
2302
  * cluster's ({@link SignInOptions}), the assertion is made for that
@@ -2627,10 +2729,51 @@ var CalimeroClient = class _CalimeroClient {
2627
2729
  await this.delete(`auth/aliases/${seg(alias)}`);
2628
2730
  }
2629
2731
  // ---------------------------------------------------------------- organizations
2630
- async createOrg(name) {
2631
- const { orgId } = await this.post("orgs", { name });
2732
+ /**
2733
+ * Create an organization. `recoveryKey` (from {@link recoveryPublicKey}) lets
2734
+ * whoever holds its seed recover the organization if its owner loses every
2735
+ * passkey; keep the seed offline ({@link recoverySeedText}).
2736
+ */
2737
+ async createOrg(name, options = {}) {
2738
+ const body = { name };
2739
+ if (options.recoveryKey !== void 0) body.recoveryKey = options.recoveryKey;
2740
+ const { orgId } = await this.post("orgs", body);
2632
2741
  return orgId;
2633
2742
  }
2743
+ /**
2744
+ * Set or rotate (`publicKey`, from {@link recoveryPublicKey}) or remove
2745
+ * (`null`) the organization's recovery key: its owner only, approved with
2746
+ * `passkey`. A recovery under way is cancelled, and the old seed's
2747
+ * signatures are refused from then on.
2748
+ */
2749
+ async setRecoveryKey(org, publicKey, passkey) {
2750
+ await this.postConfirmed(`orgs/${seg(org)}/recovery`, { publicKey }, passkey);
2751
+ }
2752
+ /**
2753
+ * Start handing `org` to the signed-in account with the recovery seed,
2754
+ * which signs here and never leaves this process. Finish with
2755
+ * {@link finishRecovery} once `readyAt` has passed; until then the owner may
2756
+ * cancel. A wrong seed (or no organization, or none set) is a `403`.
2757
+ */
2758
+ async startRecovery(org, seed) {
2759
+ const account = (await this.me()).accountId;
2760
+ const nonce2 = b64url(crypto.getRandomValues(new Uint8Array(16)));
2761
+ const signature = await signRecovery(seed, org, account, nonce2);
2762
+ return this.post(`orgs/${seg(org)}/recovery/start`, { nonce: nonce2, signature });
2763
+ }
2764
+ /** Cancel the recovery under way: the organization's owner, approved with `passkey`. */
2765
+ async cancelRecovery(org, passkey) {
2766
+ await this.postConfirmed(`orgs/${seg(org)}/recovery/cancel`, {}, passkey);
2767
+ }
2768
+ /**
2769
+ * Finish a recovery this account started, once its delay passed
2770
+ * (`409 RECOVERY_NOT_READY` before, with `readyAt`), approved with this
2771
+ * account's `passkey`. The account becomes the owner; the previous owner
2772
+ * leaves the organization.
2773
+ */
2774
+ finishRecovery(org, passkey) {
2775
+ return this.postConfirmed(`orgs/${seg(org)}/recovery/finish`, {}, passkey);
2776
+ }
2634
2777
  /** The organization, with the teams this caller reaches or manages. */
2635
2778
  getOrg(org) {
2636
2779
  return this.get(`orgs/${seg(org)}`);