@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.mjs CHANGED
@@ -135,20 +135,20 @@ async function x25519KeyPair(secret) {
135
135
  const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
136
136
  return { privateKey: pair.privateKey, publicKey };
137
137
  }
138
- const privateKey = await crypto.subtle.importKey(
138
+ const privateKey2 = await crypto.subtle.importKey(
139
139
  "pkcs8",
140
140
  concat(PKCS8_X25519_PREFIX, secret),
141
141
  { name: "X25519" },
142
142
  true,
143
143
  ["deriveBits"]
144
144
  );
145
- const jwk = await crypto.subtle.exportKey("jwk", privateKey);
146
- return { privateKey, publicKey: base64UrlDecode(jwk.x) };
145
+ const jwk = await crypto.subtle.exportKey("jwk", privateKey2);
146
+ return { privateKey: privateKey2, publicKey: base64UrlDecode(jwk.x) };
147
147
  }
148
- async function dh(privateKey, publicKey) {
148
+ async function dh(privateKey2, publicKey) {
149
149
  const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
150
150
  return new Uint8Array(
151
- await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey, 256)
151
+ await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey2, 256)
152
152
  );
153
153
  }
154
154
  function noiseNonce(n) {
@@ -1418,6 +1418,21 @@ var OriginTaken = class extends CalimeroError {
1418
1418
  this.name = "OriginTaken";
1419
1419
  }
1420
1420
  };
1421
+ var SignUpRefused = class extends CalimeroError {
1422
+ constructor(message, details = {}) {
1423
+ super(403, "SIGN_UP_REFUSED", message, details);
1424
+ this.name = "SignUpRefused";
1425
+ this.reason = signUpRefusal(message);
1426
+ }
1427
+ };
1428
+ function signUpRefusal(message) {
1429
+ if (/invite only/.test(message)) return "invite-only";
1430
+ if (/pre-authorized|sign-up invite/.test(message)) return "closed";
1431
+ if (/unknown invite/.test(message)) return "unknown-invite";
1432
+ if (/invite has expired/.test(message)) return "expired-invite";
1433
+ if (/organization is gone/.test(message)) return "org-gone";
1434
+ return "other";
1435
+ }
1421
1436
  var QuotaExceeded = class extends CalimeroError {
1422
1437
  constructor(message, details = {}) {
1423
1438
  super(429, "QUOTA_EXCEEDED", message, details);
@@ -1492,6 +1507,8 @@ function errorFrom(status, body, retryAfterHeader) {
1492
1507
  return new OriginTaken(message, details);
1493
1508
  case "429 QUOTA_EXCEEDED":
1494
1509
  return new QuotaExceeded(message, details);
1510
+ case "403 SIGN_UP_REFUSED":
1511
+ return new SignUpRefused(message, details);
1495
1512
  case "412 CONFIRM_REQUIRED":
1496
1513
  return new ConfirmRequired(message, details);
1497
1514
  case "413 ARGS_TOO_LARGE":
@@ -1568,6 +1585,72 @@ function browserPasskey() {
1568
1585
  };
1569
1586
  }
1570
1587
 
1588
+ // src/cloud-v2/recovery.ts
1589
+ var RECOVERY_DOMAIN = "calimero.org-recovery.v1\0";
1590
+ var MAX_RECOVERY_NONCE_BYTES = 64;
1591
+ var PKCS8_ED25519 = Uint8Array.from([
1592
+ 48,
1593
+ 46,
1594
+ 2,
1595
+ 1,
1596
+ 0,
1597
+ 48,
1598
+ 5,
1599
+ 6,
1600
+ 3,
1601
+ 43,
1602
+ 101,
1603
+ 112,
1604
+ 4,
1605
+ 34,
1606
+ 4,
1607
+ 32
1608
+ ]);
1609
+ function newRecoverySeed() {
1610
+ return crypto.getRandomValues(new Uint8Array(32));
1611
+ }
1612
+ function recoverySeedText(seed) {
1613
+ checkSeed(seed);
1614
+ const hex2 = Array.from(seed, (b) => b.toString(16).padStart(2, "0")).join("");
1615
+ const groups = [];
1616
+ for (let i = 0; i < hex2.length; i += 8) groups.push(hex2.slice(i, i + 8));
1617
+ return groups.join("-");
1618
+ }
1619
+ function parseRecoverySeed(text) {
1620
+ const digits = text.replace(/[-\s]/g, "");
1621
+ if (!/^[0-9a-fA-F]{64}$/.test(digits)) throw new Error("a recovery seed is 64 hex digits");
1622
+ return Uint8Array.from({ length: 32 }, (_, i) => parseInt(digits.slice(2 * i, 2 * i + 2), 16));
1623
+ }
1624
+ function recoveryMessage(org, account, nonce2) {
1625
+ return new TextEncoder().encode(`${RECOVERY_DOMAIN}${org}\0${account}\0${nonce2}`);
1626
+ }
1627
+ async function recoveryPublicKey(seed) {
1628
+ const key = await privateKey(seed, true);
1629
+ const jwk = await crypto.subtle.exportKey("jwk", key);
1630
+ if (!jwk.x) throw new Error("this runtime cannot export an Ed25519 public key");
1631
+ return jwk.x;
1632
+ }
1633
+ async function signRecovery(seed, org, account, nonce2) {
1634
+ const nonceBytes = new TextEncoder().encode(nonce2).length;
1635
+ if (nonceBytes === 0 || nonceBytes > MAX_RECOVERY_NONCE_BYTES) {
1636
+ throw new Error(`a recovery nonce is 1 to ${MAX_RECOVERY_NONCE_BYTES} bytes`);
1637
+ }
1638
+ const key = await privateKey(seed, false);
1639
+ const sig = await crypto.subtle.sign("Ed25519", key, buf2(recoveryMessage(org, account, nonce2)));
1640
+ return b64url(new Uint8Array(sig));
1641
+ }
1642
+ function checkSeed(seed) {
1643
+ if (seed.length !== 32) throw new Error("a recovery seed is 32 bytes");
1644
+ }
1645
+ function privateKey(seed, extractable) {
1646
+ checkSeed(seed);
1647
+ const pkcs8 = new Uint8Array(PKCS8_ED25519.length + 32);
1648
+ pkcs8.set(PKCS8_ED25519);
1649
+ pkcs8.set(seed, PKCS8_ED25519.length);
1650
+ return crypto.subtle.importKey("pkcs8", buf2(pkcs8), { name: "Ed25519" }, extractable, ["sign"]);
1651
+ }
1652
+ var buf2 = (b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
1653
+
1571
1654
  // src/cloud-v2/approvals.ts
1572
1655
  var MAX_REPORT_PAGE = 100;
1573
1656
  function pageQuery(page = {}) {
@@ -2060,23 +2143,33 @@ var CalimeroClient = class _CalimeroClient {
2060
2143
  * origin's relying party (the confirmation's `rpId` is that origin's too)
2061
2144
  * and binds the session to the package the origin is registered for; an
2062
2145
  * unregistered origin is refused with {@link OriginNotRegistered}.
2146
+ *
2147
+ * `invite` ({@link SignUpOptions}) goes on both halves and on the retry,
2148
+ * as the node judges it on each before it spends anything; a sign-up the
2149
+ * cluster's policy refuses, or an unknown, expired or used invite, is
2150
+ * {@link SignUpRefused} (from `begin`, before any passkey is made).
2063
2151
  */
2064
2152
  async signUp(passkey, name, options = {}) {
2065
2153
  const origin = this.pageOrigin(options);
2154
+ const invite = options.invite ? { invite: options.invite } : {};
2066
2155
  const begin = await this.begin(
2067
2156
  "auth/passkey/register/begin",
2068
- origin === void 0 ? { name } : { name, origin },
2157
+ origin === void 0 ? { name, ...invite } : { name, origin, ...invite },
2069
2158
  origin
2070
2159
  );
2071
2160
  const registration = await passkey.create(begin.options);
2072
2161
  const session = await this.postConfirmed(
2073
2162
  "auth/passkey/register/finish",
2074
- { challengeId: begin.challengeId, registration },
2163
+ { challengeId: begin.challengeId, registration, ...invite },
2075
2164
  passkey
2076
2165
  );
2077
2166
  this.useSession(session);
2078
2167
  return session;
2079
2168
  }
2169
+ /** Who may create an account on the cluster (`GET /sign-up`); needs no session. */
2170
+ signUpPolicy() {
2171
+ return this.get("sign-up");
2172
+ }
2080
2173
  /**
2081
2174
  * Sign in with an existing passkey. On a page whose origin is not the
2082
2175
  * cluster's ({@link SignInOptions}), the assertion is made for that
@@ -2509,10 +2602,51 @@ var CalimeroClient = class _CalimeroClient {
2509
2602
  await this.delete(`auth/aliases/${seg(alias)}`);
2510
2603
  }
2511
2604
  // ---------------------------------------------------------------- organizations
2512
- async createOrg(name) {
2513
- const { orgId } = await this.post("orgs", { name });
2605
+ /**
2606
+ * Create an organization. `recoveryKey` (from {@link recoveryPublicKey}) lets
2607
+ * whoever holds its seed recover the organization if its owner loses every
2608
+ * passkey; keep the seed offline ({@link recoverySeedText}).
2609
+ */
2610
+ async createOrg(name, options = {}) {
2611
+ const body = { name };
2612
+ if (options.recoveryKey !== void 0) body.recoveryKey = options.recoveryKey;
2613
+ const { orgId } = await this.post("orgs", body);
2514
2614
  return orgId;
2515
2615
  }
2616
+ /**
2617
+ * Set or rotate (`publicKey`, from {@link recoveryPublicKey}) or remove
2618
+ * (`null`) the organization's recovery key: its owner only, approved with
2619
+ * `passkey`. A recovery under way is cancelled, and the old seed's
2620
+ * signatures are refused from then on.
2621
+ */
2622
+ async setRecoveryKey(org, publicKey, passkey) {
2623
+ await this.postConfirmed(`orgs/${seg(org)}/recovery`, { publicKey }, passkey);
2624
+ }
2625
+ /**
2626
+ * Start handing `org` to the signed-in account with the recovery seed,
2627
+ * which signs here and never leaves this process. Finish with
2628
+ * {@link finishRecovery} once `readyAt` has passed; until then the owner may
2629
+ * cancel. A wrong seed (or no organization, or none set) is a `403`.
2630
+ */
2631
+ async startRecovery(org, seed) {
2632
+ const account = (await this.me()).accountId;
2633
+ const nonce2 = b64url(crypto.getRandomValues(new Uint8Array(16)));
2634
+ const signature = await signRecovery(seed, org, account, nonce2);
2635
+ return this.post(`orgs/${seg(org)}/recovery/start`, { nonce: nonce2, signature });
2636
+ }
2637
+ /** Cancel the recovery under way: the organization's owner, approved with `passkey`. */
2638
+ async cancelRecovery(org, passkey) {
2639
+ await this.postConfirmed(`orgs/${seg(org)}/recovery/cancel`, {}, passkey);
2640
+ }
2641
+ /**
2642
+ * Finish a recovery this account started, once its delay passed
2643
+ * (`409 RECOVERY_NOT_READY` before, with `readyAt`), approved with this
2644
+ * account's `passkey`. The account becomes the owner; the previous owner
2645
+ * leaves the organization.
2646
+ */
2647
+ finishRecovery(org, passkey) {
2648
+ return this.postConfirmed(`orgs/${seg(org)}/recovery/finish`, {}, passkey);
2649
+ }
2516
2650
  /** The organization, with the teams this caller reaches or manages. */
2517
2651
  getOrg(org) {
2518
2652
  return this.get(`orgs/${seg(org)}`);
@@ -3359,6 +3493,7 @@ export {
3359
3493
  IdempotencyMismatch,
3360
3494
  KMS_RELEASE_SIGNER,
3361
3495
  LoginRequired,
3496
+ MAX_RECOVERY_NONCE_BYTES,
3362
3497
  MAX_REPORT_PAGE,
3363
3498
  MOCK_QUOTE_HEADER,
3364
3499
  NODE_RELEASE_SIGNER,
@@ -3368,11 +3503,13 @@ export {
3368
3503
  OriginTaken,
3369
3504
  PRODUCTION_PROFILE,
3370
3505
  QuotaExceeded,
3506
+ RECOVERY_DOMAIN,
3371
3507
  RELEASE_VERIFIER,
3372
3508
  ReleaseDeadlineError,
3373
3509
  SEALED_CONTENT_TYPE,
3374
3510
  SEALED_PATH,
3375
3511
  SealedTransportError,
3512
+ SignUpRefused,
3376
3513
  StaleTransportKeyError,
3377
3514
  TrapError,
3378
3515
  UnavailableError,
@@ -3400,18 +3537,24 @@ export {
3400
3537
  fromB64url,
3401
3538
  frontendManifest,
3402
3539
  generateOwnerKey,
3540
+ newRecoverySeed,
3403
3541
  orgCapabilities,
3404
3542
  ownerPublicKey,
3405
3543
  pageDeployment,
3406
3544
  pageQuery,
3545
+ parseRecoverySeed,
3407
3546
  pendingApprovalsFrom,
3408
3547
  provisionAgent,
3409
3548
  provisionInfo,
3410
3549
  publisherBindingBytes,
3411
3550
  publisherBindingMessage,
3551
+ recoveryMessage,
3552
+ recoveryPublicKey,
3553
+ recoverySeedText,
3412
3554
  releaseAgent,
3413
3555
  releaseConsent,
3414
3556
  releaseNotesUrl,
3557
+ signRecovery,
3415
3558
  stalledBy,
3416
3559
  transportKeyBinding,
3417
3560
  trustSignedRelease,