@calimero-network/mero-js 19.8.0 → 19.9.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
@@ -17,8 +17,8 @@ export type { Codec, EphemeralEntry } from './ephemeral/index.js';
17
17
  export * from './cloud/index.js';
18
18
  export * from './relay/index.js';
19
19
  export { signWarrant, intentHash } from './warrant/index.js';
20
- export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account/index.js';
21
- export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, } from './account/index.js';
20
+ export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account/index.js';
21
+ export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, AccountLoginInput, } from './account/index.js';
22
22
  export { signDeviceCert, mintDeviceId, deviceCertPayload, accountForRoot, } from './device-cert/index.js';
23
23
  export type { DeviceCertInput } from './device-cert/index.js';
24
24
  export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from './namespace-op/index.js';
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ export * from './relay/index.js';
25
25
  // Warrant signing — mint the author's consent for a relay to run one intent
26
26
  export { signWarrant, intentHash } from './warrant/index.js';
27
27
  // Account roots — mint one with a recovery phrase, or prove you hold one
28
- export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account/index.js';
28
+ export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account/index.js';
29
29
  export { signDeviceCert, mintDeviceId, deviceCertPayload, accountForRoot, } from './device-cert/index.js';
30
30
  export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from './namespace-op/index.js';
31
31
  // Login-statement signing — the device's half of a password-free session
package/dist/index.mjs CHANGED
@@ -36,10 +36,13 @@ var TERMINAL_AUTH_ERRORS = /* @__PURE__ */ new Set(["token_reuse", "token_revoke
36
36
  function extractErrorMessage(bodyText) {
37
37
  if (!bodyText) return void 0;
38
38
  try {
39
- const error = JSON.parse(bodyText)?.error;
39
+ const body = JSON.parse(bodyText);
40
+ const error = body?.error;
40
41
  if (typeof error === "string" && error.trim() !== "") return error;
41
42
  const nested = error?.message;
42
43
  if (typeof nested === "string" && nested.trim() !== "") return nested;
44
+ const detail = body?.detail;
45
+ if (typeof detail === "string" && detail.trim() !== "") return detail;
43
46
  } catch {
44
47
  }
45
48
  return void 0;
@@ -4661,6 +4664,9 @@ var ENGLISH_WORDLIST = [
4661
4664
  var ACCOUNT_LINK_DOMAIN = new TextEncoder().encode(
4662
4665
  "calimero.mdma.account-link.v1\0"
4663
4666
  );
4667
+ var ACCOUNT_LOGIN_DOMAIN = new TextEncoder().encode(
4668
+ "calimero.mdma.account-login.v1\0"
4669
+ );
4664
4670
  var ENTROPY_BITS = 256;
4665
4671
  var CHECKSUM_BITS = ENTROPY_BITS / 32;
4666
4672
  var PHRASE_WORDS = (ENTROPY_BITS + CHECKSUM_BITS) / 11;
@@ -4692,6 +4698,18 @@ async function signAccountLink(input) {
4692
4698
  );
4693
4699
  return base64(signature);
4694
4700
  }
4701
+ async function signAccountLogin(input) {
4702
+ if (!input.nonce) throw new Error("nonce is required to sign an account login");
4703
+ const key = await importSigningKey(input.rootSecret);
4704
+ const signature = new Uint8Array(
4705
+ await crypto.subtle.sign(
4706
+ "Ed25519",
4707
+ key,
4708
+ concat(ACCOUNT_LOGIN_DOMAIN, new TextEncoder().encode(input.nonce))
4709
+ )
4710
+ );
4711
+ return base64(signature);
4712
+ }
4695
4713
  async function withPhrase(secret, phrase) {
4696
4714
  return { ...await accountRootFromSecret(secret), phrase };
4697
4715
  }
@@ -4822,6 +4840,70 @@ var CloudClient = class {
4822
4840
  const body = await this.request("POST", "/api/auth/google", { body: { id_token: idToken }, anonymous: true });
4823
4841
  return this.adoptSession(body);
4824
4842
  }
4843
+ /**
4844
+ * Sign in as a Calimero account, proving it with its root key.
4845
+ *
4846
+ * The password-free counterpart to {@link signInWithGoogle}: no Google, no
4847
+ * password, no server-held secret — the cloud mints a challenge, the account
4848
+ * root signs it, and the session that comes back is the same MDMA session
4849
+ * token every other cloud route already accepts.
4850
+ *
4851
+ * Doing this **once** is what makes an account's later device proofs mean
4852
+ * something. A routing read shows that the caller holds a device the root
4853
+ * certified, but certificates are public, so it can never show that anyone
4854
+ * held the root; the cloud records this claim and from then on knows the
4855
+ * account behind those reads was claimed by its owner. The claim is recorded
4856
+ * even when the session is refused.
4857
+ *
4858
+ * @param rootSecret the account root's signing secret, 64 hex. It never
4859
+ * leaves this process — only a signature over the cloud's nonce travels.
4860
+ *
4861
+ * A `403` means either the signature did not verify, or the account is not
4862
+ * linked to a cloud login: ownership alone says who you are, and the link
4863
+ * says what you are entitled to. Anyone can mint a root offline, so a session
4864
+ * on the proof alone would authenticate perfectly and authorize nothing. The
4865
+ * ownership claim is recorded either way, so linking later needs no re-proof.
4866
+ */
4867
+ async signInWithAccount(rootSecret) {
4868
+ const root = await accountRootFromSecret(rootSecret);
4869
+ const { nonce } = await this.getAccountLoginChallenge();
4870
+ return this.submitAccountLogin({
4871
+ rootPublicKey: root.publicKey,
4872
+ nonce,
4873
+ signature: await signAccountLogin({ rootSecret, nonce })
4874
+ });
4875
+ }
4876
+ /**
4877
+ * Mint a challenge for an account root to sign.
4878
+ *
4879
+ * Public, and deliberately so: the caller has no session — obtaining one is
4880
+ * what this exchange does. The nonce is not a capability, it is the thing a
4881
+ * capability gets demonstrated against, and demonstrating it needs the root.
4882
+ *
4883
+ * Split out of {@link signInWithAccount} for the case that call cannot serve:
4884
+ * a root held by a hardware key, a desktop app or an air-gapped machine,
4885
+ * which signs without the secret ever reaching this process.
4886
+ */
4887
+ async getAccountLoginChallenge() {
4888
+ const body = await this.request(
4889
+ "GET",
4890
+ "/api/auth/account/challenge",
4891
+ { anonymous: true }
4892
+ );
4893
+ return { nonce: body.nonce, expiresAtMs: body.expires_at_ms };
4894
+ }
4895
+ /** Post a proof assembled elsewhere. The other half of {@link getAccountLoginChallenge}. */
4896
+ async submitAccountLogin(proof) {
4897
+ const body = await this.request("POST", "/api/auth/account", {
4898
+ body: {
4899
+ root_public_key: proof.rootPublicKey,
4900
+ nonce: proof.nonce,
4901
+ signature: proof.signature
4902
+ },
4903
+ anonymous: true
4904
+ });
4905
+ return this.adoptSession(body);
4906
+ }
4825
4907
  /**
4826
4908
  * Refresh the session token explicitly.
4827
4909
  *
@@ -6388,6 +6470,7 @@ export {
6388
6470
  probeNodeHealth,
6389
6471
  routingProofHeaders,
6390
6472
  signAccountLink,
6473
+ signAccountLogin,
6391
6474
  signDeviceCert,
6392
6475
  signLoginStatement,
6393
6476
  signMemberJoinOp,