@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.
@@ -74,3 +74,28 @@ export interface AccountLinkInput {
74
74
  * serve.
75
75
  */
76
76
  export declare function signAccountLink(input: AccountLinkInput): Promise<string>;
77
+ /** What {@link signAccountLogin} needs. */
78
+ export interface AccountLoginInput {
79
+ /** The account root's signing secret, 64 hex. */
80
+ rootSecret: string;
81
+ /** The challenge from `GET /api/auth/account/challenge`, verbatim UTF-8. */
82
+ nonce: string;
83
+ }
84
+ /**
85
+ * Sign a cloud login challenge with the account root. Returns base64.
86
+ *
87
+ * The statement this makes is *"I hold the root of account X"*, and it is the
88
+ * one statement a device credential can never make. A routing read proves the
89
+ * caller holds a device the root certified — the credential carries the root's
90
+ * signature over that device — but a certificate is public and travels in the
91
+ * clear inside every device-link op, so anyone who has seen one can say "a
92
+ * device of X is asking". Only the root can say the account is theirs.
93
+ *
94
+ * Which is why this is worth doing **once**: the cloud records the claim, and
95
+ * from then on it knows the account behind a device proof was proven by its
96
+ * owner. The session it also mints is the ordinary MDMA session token, accepted
97
+ * everywhere a Google-issued one is.
98
+ *
99
+ * Standard base64, not base64url — mdma decodes with `validate=True`.
100
+ */
101
+ export declare function signAccountLogin(input: AccountLoginInput): Promise<string>;
Binary file
@@ -1,2 +1,2 @@
1
- export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account.js';
2
- export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, } from './account.js';
1
+ export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account.js';
2
+ export type { AccountRoot, RecoverableAccountRoot, AccountLinkInput, AccountLoginInput, } from './account.js';
@@ -1,2 +1,2 @@
1
1
  // Account roots — minting one, restoring it from a phrase, and proving you hold it.
2
- export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, } from './account.js';
2
+ export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from './account.js';
@@ -240,6 +240,28 @@ export interface AccountLinkProof {
240
240
  /** `signAccountLink`'s output — standard base64. */
241
241
  signature: string;
242
242
  }
243
+ /** A single-use challenge for an account root to sign itself in with. */
244
+ export interface AccountLoginChallenge {
245
+ /** Sign this verbatim. Opaque and sealed by the cloud; nonces live ~2 minutes. */
246
+ nonce: string;
247
+ /** When it stops being accepted, epoch milliseconds. */
248
+ expiresAtMs: number;
249
+ }
250
+ /** A proof assembled by whoever holds the root, for {@link CloudClient.submitAccountLogin}. */
251
+ export interface AccountLoginProof {
252
+ /**
253
+ * The root that names the account, 64 hex.
254
+ *
255
+ * No `accountId` field, unlike {@link AccountLinkProof}: the cloud derives
256
+ * the account from this key rather than accepting one alongside it, so there
257
+ * is nothing a caller could state that the signature would then contradict.
258
+ */
259
+ rootPublicKey: string;
260
+ /** The challenge that was signed. */
261
+ nonce: string;
262
+ /** `signAccountLogin`'s output — standard base64. */
263
+ signature: string;
264
+ }
243
265
  /** The outcome of {@link CloudClient.linkAccount}. */
244
266
  export interface CloudAccountLink {
245
267
  accountId: string;
@@ -282,6 +304,45 @@ export declare class CloudClient {
282
304
  * be kept or re-minted per call.
283
305
  */
284
306
  signInWithGoogle(idToken: string): Promise<CloudSession>;
307
+ /**
308
+ * Sign in as a Calimero account, proving it with its root key.
309
+ *
310
+ * The password-free counterpart to {@link signInWithGoogle}: no Google, no
311
+ * password, no server-held secret — the cloud mints a challenge, the account
312
+ * root signs it, and the session that comes back is the same MDMA session
313
+ * token every other cloud route already accepts.
314
+ *
315
+ * Doing this **once** is what makes an account's later device proofs mean
316
+ * something. A routing read shows that the caller holds a device the root
317
+ * certified, but certificates are public, so it can never show that anyone
318
+ * held the root; the cloud records this claim and from then on knows the
319
+ * account behind those reads was claimed by its owner. The claim is recorded
320
+ * even when the session is refused.
321
+ *
322
+ * @param rootSecret the account root's signing secret, 64 hex. It never
323
+ * leaves this process — only a signature over the cloud's nonce travels.
324
+ *
325
+ * A `403` means either the signature did not verify, or the account is not
326
+ * linked to a cloud login: ownership alone says who you are, and the link
327
+ * says what you are entitled to. Anyone can mint a root offline, so a session
328
+ * on the proof alone would authenticate perfectly and authorize nothing. The
329
+ * ownership claim is recorded either way, so linking later needs no re-proof.
330
+ */
331
+ signInWithAccount(rootSecret: string): Promise<CloudSession>;
332
+ /**
333
+ * Mint a challenge for an account root to sign.
334
+ *
335
+ * Public, and deliberately so: the caller has no session — obtaining one is
336
+ * what this exchange does. The nonce is not a capability, it is the thing a
337
+ * capability gets demonstrated against, and demonstrating it needs the root.
338
+ *
339
+ * Split out of {@link signInWithAccount} for the case that call cannot serve:
340
+ * a root held by a hardware key, a desktop app or an air-gapped machine,
341
+ * which signs without the secret ever reaching this process.
342
+ */
343
+ getAccountLoginChallenge(): Promise<AccountLoginChallenge>;
344
+ /** Post a proof assembled elsewhere. The other half of {@link getAccountLoginChallenge}. */
345
+ submitAccountLogin(proof: AccountLoginProof): Promise<CloudSession>;
285
346
  /**
286
347
  * Refresh the session token explicitly.
287
348
  *
@@ -24,7 +24,7 @@
24
24
  * ever sent, and would be a throughput bottleneck on a path that has no reason
25
25
  * to involve it.
26
26
  */
27
- import { accountRootFromSecret, signAccountLink, } from '../account/index.js';
27
+ import { accountRootFromSecret, signAccountLink, signAccountLogin, } from '../account/index.js';
28
28
  import { HTTPError } from '../http-client/web-client.js';
29
29
  import { routingProofHeaders, } from './routing-proof.js';
30
30
  /** Where the hosted cloud lives when a caller names no other. */
@@ -77,6 +77,66 @@ export class CloudClient {
77
77
  const body = await this.request('POST', '/api/auth/google', { body: { id_token: idToken }, anonymous: true });
78
78
  return this.adoptSession(body);
79
79
  }
80
+ /**
81
+ * Sign in as a Calimero account, proving it with its root key.
82
+ *
83
+ * The password-free counterpart to {@link signInWithGoogle}: no Google, no
84
+ * password, no server-held secret — the cloud mints a challenge, the account
85
+ * root signs it, and the session that comes back is the same MDMA session
86
+ * token every other cloud route already accepts.
87
+ *
88
+ * Doing this **once** is what makes an account's later device proofs mean
89
+ * something. A routing read shows that the caller holds a device the root
90
+ * certified, but certificates are public, so it can never show that anyone
91
+ * held the root; the cloud records this claim and from then on knows the
92
+ * account behind those reads was claimed by its owner. The claim is recorded
93
+ * even when the session is refused.
94
+ *
95
+ * @param rootSecret the account root's signing secret, 64 hex. It never
96
+ * leaves this process — only a signature over the cloud's nonce travels.
97
+ *
98
+ * A `403` means either the signature did not verify, or the account is not
99
+ * linked to a cloud login: ownership alone says who you are, and the link
100
+ * says what you are entitled to. Anyone can mint a root offline, so a session
101
+ * on the proof alone would authenticate perfectly and authorize nothing. The
102
+ * ownership claim is recorded either way, so linking later needs no re-proof.
103
+ */
104
+ async signInWithAccount(rootSecret) {
105
+ const root = await accountRootFromSecret(rootSecret);
106
+ const { nonce } = await this.getAccountLoginChallenge();
107
+ return this.submitAccountLogin({
108
+ rootPublicKey: root.publicKey,
109
+ nonce,
110
+ signature: await signAccountLogin({ rootSecret, nonce }),
111
+ });
112
+ }
113
+ /**
114
+ * Mint a challenge for an account root to sign.
115
+ *
116
+ * Public, and deliberately so: the caller has no session — obtaining one is
117
+ * what this exchange does. The nonce is not a capability, it is the thing a
118
+ * capability gets demonstrated against, and demonstrating it needs the root.
119
+ *
120
+ * Split out of {@link signInWithAccount} for the case that call cannot serve:
121
+ * a root held by a hardware key, a desktop app or an air-gapped machine,
122
+ * which signs without the secret ever reaching this process.
123
+ */
124
+ async getAccountLoginChallenge() {
125
+ const body = await this.request('GET', '/api/auth/account/challenge', { anonymous: true });
126
+ return { nonce: body.nonce, expiresAtMs: body.expires_at_ms };
127
+ }
128
+ /** Post a proof assembled elsewhere. The other half of {@link getAccountLoginChallenge}. */
129
+ async submitAccountLogin(proof) {
130
+ const body = await this.request('POST', '/api/auth/account', {
131
+ body: {
132
+ root_public_key: proof.rootPublicKey,
133
+ nonce: proof.nonce,
134
+ signature: proof.signature,
135
+ },
136
+ anonymous: true,
137
+ });
138
+ return this.adoptSession(body);
139
+ }
80
140
  /**
81
141
  * Refresh the session token explicitly.
82
142
  *
@@ -1,5 +1,5 @@
1
1
  export { CloudClient } from './cloud-client.js';
2
- export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
2
+ export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, AccountLoginChallenge, AccountLoginProof, EnableHAOptions, DisableHAOptions, } from './cloud-client.js';
3
3
  export { connectCloud } from './connect.js';
4
4
  export type { ConnectCloudOptions, CloudConnection } from './connect.js';
5
5
  export { signRoutingChallenge, routingProofHeaders } from './routing-proof.js';
@@ -18,12 +18,20 @@ function extractErrorMessage(bodyText) {
18
18
  if (!bodyText)
19
19
  return undefined;
20
20
  try {
21
- const error = JSON.parse(bodyText)?.error;
21
+ const body = JSON.parse(bodyText);
22
+ const error = body?.error;
22
23
  if (typeof error === 'string' && error.trim() !== '')
23
24
  return error;
24
25
  const nested = error?.message;
25
26
  if (typeof nested === 'string' && nested.trim() !== '')
26
27
  return nested;
28
+ // FastAPI's spelling, which is what every Calimero Cloud route answers
29
+ // with. Without it a manager's carefully worded refusal — "ownership is
30
+ // recorded, but the account is not linked" — reached callers as a bare
31
+ // "HTTP 403", and the one thing they could do about it was invisible.
32
+ const detail = body?.detail;
33
+ if (typeof detail === 'string' && detail.trim() !== '')
34
+ return detail;
27
35
  }
28
36
  catch {
29
37
  // Not JSON — the status line is all we can honestly report.