@calimero-network/mero-js 29.3.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,6 +60,7 @@ __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,
@@ -92,18 +94,24 @@ __export(index_exports, {
92
94
  fromB64url: () => fromB64url,
93
95
  frontendManifest: () => frontendManifest,
94
96
  generateOwnerKey: () => generateOwnerKey,
97
+ newRecoverySeed: () => newRecoverySeed,
95
98
  orgCapabilities: () => orgCapabilities,
96
99
  ownerPublicKey: () => ownerPublicKey,
97
100
  pageDeployment: () => pageDeployment,
98
101
  pageQuery: () => pageQuery,
102
+ parseRecoverySeed: () => parseRecoverySeed,
99
103
  pendingApprovalsFrom: () => pendingApprovalsFrom,
100
104
  provisionAgent: () => provisionAgent,
101
105
  provisionInfo: () => provisionInfo,
102
106
  publisherBindingBytes: () => publisherBindingBytes,
103
107
  publisherBindingMessage: () => publisherBindingMessage,
108
+ recoveryMessage: () => recoveryMessage,
109
+ recoveryPublicKey: () => recoveryPublicKey,
110
+ recoverySeedText: () => recoverySeedText,
104
111
  releaseAgent: () => releaseAgent,
105
112
  releaseConsent: () => releaseConsent,
106
113
  releaseNotesUrl: () => releaseNotesUrl,
114
+ signRecovery: () => signRecovery,
107
115
  stalledBy: () => stalledBy,
108
116
  transportKeyBinding: () => transportKeyBinding,
109
117
  trustSignedRelease: () => trustSignedRelease,
@@ -254,20 +262,20 @@ async function x25519KeyPair(secret) {
254
262
  const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
255
263
  return { privateKey: pair.privateKey, publicKey };
256
264
  }
257
- const privateKey = await crypto.subtle.importKey(
265
+ const privateKey2 = await crypto.subtle.importKey(
258
266
  "pkcs8",
259
267
  concat(PKCS8_X25519_PREFIX, secret),
260
268
  { name: "X25519" },
261
269
  true,
262
270
  ["deriveBits"]
263
271
  );
264
- const jwk = await crypto.subtle.exportKey("jwk", privateKey);
265
- return { privateKey, publicKey: base64UrlDecode(jwk.x) };
272
+ const jwk = await crypto.subtle.exportKey("jwk", privateKey2);
273
+ return { privateKey: privateKey2, publicKey: base64UrlDecode(jwk.x) };
266
274
  }
267
- async function dh(privateKey, publicKey) {
275
+ async function dh(privateKey2, publicKey) {
268
276
  const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
269
277
  return new Uint8Array(
270
- await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey, 256)
278
+ await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey2, 256)
271
279
  );
272
280
  }
273
281
  function noiseNonce(n) {
@@ -1704,6 +1712,72 @@ function browserPasskey() {
1704
1712
  };
1705
1713
  }
1706
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
+
1707
1781
  // src/cloud-v2/approvals.ts
1708
1782
  var MAX_REPORT_PAGE = 100;
1709
1783
  function pageQuery(page = {}) {
@@ -2655,10 +2729,51 @@ var CalimeroClient = class _CalimeroClient {
2655
2729
  await this.delete(`auth/aliases/${seg(alias)}`);
2656
2730
  }
2657
2731
  // ---------------------------------------------------------------- organizations
2658
- async createOrg(name) {
2659
- 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);
2660
2741
  return orgId;
2661
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
+ }
2662
2777
  /** The organization, with the teams this caller reaches or manages. */
2663
2778
  getOrg(org) {
2664
2779
  return this.get(`orgs/${seg(org)}`);